onejs-react 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onejs-react",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "React 19 renderer for OneJS (Unity UI Toolkit)",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/hooks.ts ADDED
@@ -0,0 +1,216 @@
1
+ import { useState, useEffect, useRef } from "react"
2
+
3
+ /**
4
+ * Syncs a value from C# (or any external source) to React state, checking every frame.
5
+ *
6
+ * This hook eliminates the need for C# events or codegen - just read any property
7
+ * and React will automatically update when it changes.
8
+ *
9
+ * @param getter - Function that returns the current value (called every frame)
10
+ * @param deps - Optional dependency array (if getter depends on changing references)
11
+ * @returns The current value, updated each frame if changed
12
+ *
13
+ * @example
14
+ * // Sync a C# property to React
15
+ * const health = useFrameSync(() => player.health)
16
+ * const position = useFrameSync(() => transform.position)
17
+ *
18
+ * @example
19
+ * // With dependencies (if the object reference can change)
20
+ * const health = useFrameSync(() => currentPlayer.health, [currentPlayer])
21
+ *
22
+ * @example
23
+ * // Derived values work too
24
+ * const healthPercent = useFrameSync(() => player.health / player.maxHealth * 100)
25
+ */
26
+ export function useFrameSync<T>(getter: () => T, deps: readonly unknown[] = []): T {
27
+ // Safely get initial value
28
+ const getInitialValue = (): T => {
29
+ try {
30
+ return getter()
31
+ } catch {
32
+ return undefined as T
33
+ }
34
+ }
35
+
36
+ const [value, setValue] = useState<T>(getInitialValue)
37
+ const lastValueRef = useRef<T>(value)
38
+ const getterRef = useRef(getter)
39
+ const runningRef = useRef(false)
40
+
41
+ // Keep getter ref updated
42
+ getterRef.current = getter
43
+
44
+ useEffect(() => {
45
+ // Re-initialize when deps change
46
+ try {
47
+ const initial = getterRef.current()
48
+ lastValueRef.current = initial
49
+ setValue(initial)
50
+ } catch {
51
+ // Getter failed, keep current value
52
+ }
53
+
54
+ runningRef.current = true
55
+
56
+ const check = () => {
57
+ if (!runningRef.current) return
58
+
59
+ try {
60
+ const current = getterRef.current()
61
+ if (!Object.is(current, lastValueRef.current)) {
62
+ lastValueRef.current = current
63
+ setValue(current)
64
+ }
65
+ } catch {
66
+ // Getter might fail if object was destroyed - that's ok
67
+ }
68
+
69
+ if (runningRef.current) {
70
+ requestAnimationFrame(check)
71
+ }
72
+ }
73
+
74
+ requestAnimationFrame(check)
75
+
76
+ return () => {
77
+ runningRef.current = false
78
+ }
79
+ }, deps)
80
+
81
+ return value
82
+ }
83
+
84
+ /**
85
+ * Similar to useFrameSync but with a custom equality function.
86
+ * Useful for objects/structs where reference equality isn't sufficient.
87
+ *
88
+ * @param getter - Function that returns the current value
89
+ * @param isEqual - Custom equality function
90
+ * @param deps - Optional dependency array
91
+ *
92
+ * @example
93
+ * // Sync a Vector3, comparing by value not reference
94
+ * const pos = useFrameSyncWith(
95
+ * () => transform.position,
96
+ * (a, b) => a.x === b.x && a.y === b.y && a.z === b.z
97
+ * )
98
+ */
99
+ export function useFrameSyncWith<T>(
100
+ getter: () => T,
101
+ isEqual: (a: T, b: T) => boolean,
102
+ deps: readonly unknown[] = []
103
+ ): T {
104
+ const getInitialValue = (): T => {
105
+ try {
106
+ return getter()
107
+ } catch {
108
+ return undefined as T
109
+ }
110
+ }
111
+
112
+ const [value, setValue] = useState<T>(getInitialValue)
113
+ const lastValueRef = useRef<T>(value)
114
+ const getterRef = useRef(getter)
115
+ const isEqualRef = useRef(isEqual)
116
+ const runningRef = useRef(false)
117
+
118
+ getterRef.current = getter
119
+ isEqualRef.current = isEqual
120
+
121
+ useEffect(() => {
122
+ try {
123
+ const initial = getterRef.current()
124
+ lastValueRef.current = initial
125
+ setValue(initial)
126
+ } catch {
127
+ // Getter failed, keep current value
128
+ }
129
+
130
+ runningRef.current = true
131
+
132
+ const check = () => {
133
+ if (!runningRef.current) return
134
+
135
+ try {
136
+ const current = getterRef.current()
137
+ if (!isEqualRef.current(current, lastValueRef.current)) {
138
+ lastValueRef.current = current
139
+ setValue(current)
140
+ }
141
+ } catch {
142
+ // Getter might fail if object was destroyed
143
+ }
144
+
145
+ if (runningRef.current) {
146
+ requestAnimationFrame(check)
147
+ }
148
+ }
149
+
150
+ requestAnimationFrame(check)
151
+
152
+ return () => {
153
+ runningRef.current = false
154
+ }
155
+ }, deps)
156
+
157
+ return value
158
+ }
159
+
160
+ /**
161
+ * Throttled version of useFrameSync that only checks at a specified interval.
162
+ * Useful when you don't need per-frame updates and want to reduce overhead.
163
+ *
164
+ * @param getter - Function that returns the current value
165
+ * @param intervalMs - How often to check for changes (in milliseconds)
166
+ * @param deps - Optional dependency array
167
+ *
168
+ * @example
169
+ * // Check every 100ms instead of every frame
170
+ * const score = useThrottledSync(() => gameState.score, 100)
171
+ */
172
+ export function useThrottledSync<T>(
173
+ getter: () => T,
174
+ intervalMs: number,
175
+ deps: readonly unknown[] = []
176
+ ): T {
177
+ const getInitialValue = (): T => {
178
+ try {
179
+ return getter()
180
+ } catch {
181
+ return undefined as T
182
+ }
183
+ }
184
+
185
+ const [value, setValue] = useState<T>(getInitialValue)
186
+ const lastValueRef = useRef<T>(value)
187
+ const getterRef = useRef(getter)
188
+
189
+ getterRef.current = getter
190
+
191
+ useEffect(() => {
192
+ try {
193
+ const initial = getterRef.current()
194
+ lastValueRef.current = initial
195
+ setValue(initial)
196
+ } catch {
197
+ // Getter failed, keep current value
198
+ }
199
+
200
+ const id = setInterval(() => {
201
+ try {
202
+ const current = getterRef.current()
203
+ if (!Object.is(current, lastValueRef.current)) {
204
+ lastValueRef.current = current
205
+ setValue(current)
206
+ }
207
+ } catch {
208
+ // Getter might fail if object was destroyed
209
+ }
210
+ }, intervalMs)
211
+
212
+ return () => clearInterval(id)
213
+ }, [...deps, intervalMs])
214
+
215
+ return value
216
+ }
package/src/index.ts CHANGED
@@ -38,6 +38,9 @@ export type {
38
38
  // Vector Drawing
39
39
  export { Transform2D, useVectorContent } from './vector';
40
40
 
41
+ // Sync Hooks (C# interop)
42
+ export { useFrameSync, useFrameSyncWith, useThrottledSync } from './hooks';
43
+
41
44
  // Types
42
45
  export type {
43
46
  ViewStyle,