@blender-ui/react 0.2.0 → 0.2.1

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/hooks.js CHANGED
@@ -1,397 +1,397 @@
1
- // React's hooks, by React's names, with signals for state.
2
- //
3
- // `useState`, `useReducer`, `useMemo` ... take React's arguments, run at
4
- // React's moments and return React's shapes, but the state they hold is a
5
- // signal. Pass it as a prop or child (`<span>{count}</span>`) and a change
6
- // updates that one node: the component doesn't re-render. Read it while
7
- // rendering (`count.value`, `count + 1`, a condition) and the component
8
- // re-renders when it changes, exactly as with React's own hooks.
9
- //
10
- // The second part needs to know which component is rendering: components
11
- // created through this package's JSX runtime (`jsxImportSource:
12
- // "@blender-ui/react"`) or `createElement` are wrapped with `track()`.
13
- //
14
- // Built only on React's public hooks (useState, useReducer, useRef, useMemo,
15
- // the effect hooks, startTransition), so React's semantics (StrictMode,
16
- // concurrent rendering) come from React itself. test/hooks.test.js runs each
17
- // hook beside React's original and fails when React adds a hook that has
18
- // neither a signal version here nor an entry in NO_TWIN.
19
- //
20
- // SPDX-License-Identifier: GPL-2.0-or-later
21
- 'use strict'
22
-
23
- const React = require('react')
24
- const S = require('@blender-ui/native')
25
- const { signal, memo, effect, batch, untrack, isSignal, Tracker, Ref } = S
26
-
27
- /** React hooks re-exported unchanged, and why (the coverage test reads this). */
28
- const NO_TWIN = {
29
- use: 'suspends rendering: inherently a render-time read',
30
- useContext: 'pass a signal as the context value: consumers bind to it without re-rendering',
31
- useDebugValue: 'no state',
32
- useId: 'no state',
33
- useImperativeHandle: 'no state (works with useRef)',
34
- }
35
-
36
- // The latest value, for callbacks that outlive the render that made them.
37
- function useLatest (value) {
38
- const ref = React.useRef(value)
39
- ref.current = value
40
- return ref
41
- }
42
-
43
- /** A component-owned value, created once. */
44
- function useOnce (create) {
45
- return React.useState(create)[0]
46
- }
47
-
48
- // ---- render tracking -----------------------------------------------------------
49
-
50
- const bump = (n) => n + 1
51
-
52
- /** Re-render this component when a signal its render read changes. */
53
- function useRenderTracker () {
54
- const [, force] = React.useReducer(bump, 0)
55
- const tracker = useOnce(() => new Tracker(() => force()))
56
- const reads = React.useRef(null)
57
- // after each commit: follow what that render read
58
- React.useLayoutEffect(() => { tracker.commit(reads.current) })
59
- // mount / unmount (StrictMode re-mounts: pause, then resume)
60
- React.useLayoutEffect(() => {
61
- tracker.resume()
62
- return () => tracker.pause()
63
- }, [tracker])
64
- return { tracker, reads }
65
- }
66
-
67
- const tracked = new WeakMap()
68
- const MEMO = Symbol.for('react.memo')
69
- const FORWARD_REF = Symbol.for('react.forward_ref')
70
-
71
- /** `type` with render tracking: function components, `memo` and `forwardRef`. */
72
- function track (type) {
73
- if (type == null) return type
74
- if (typeof type === 'function') {
75
- if (type.prototype?.isReactComponent) return type // class components: no hooks
76
- } else if (typeof type !== 'object' || (type.$$typeof !== MEMO && type.$$typeof !== FORWARD_REF)) {
77
- return type // host components, fragments, providers, lazy ...
78
- }
79
- let t = tracked.get(type)
80
- if (t !== undefined) return t
81
- if (typeof type === 'function') {
82
- t = function (props, secondArg) {
83
- const { tracker, reads } = useRenderTracker()
84
- const [result, r] = tracker.run(() => type(props, secondArg))
85
- reads.current = r
86
- return result
87
- }
88
- Object.defineProperty(t, 'name', { value: type.name })
89
- t.displayName = type.displayName || type.name
90
- } else if (type.$$typeof === MEMO) {
91
- t = React.memo(track(type.type), type.compare)
92
- t.displayName = type.displayName
93
- } else {
94
- t = React.forwardRef(track(type.render))
95
- t.displayName = type.displayName
96
- }
97
- tracked.set(type, t)
98
- tracked.set(t, t)
99
- return t
100
- }
101
-
102
- /** React's `createElement`, with render tracking for components. */
103
- function createElement (type, props, ...children) {
104
- return React.createElement(track(type), props, ...children)
105
- }
106
-
107
- // ---- state -----------------------------------------------------------------
108
-
109
- /** Rust's `use_signal`: a signal owned by this component. */
110
- function useSignal (initial) {
111
- return useOnce(() => signal(typeof initial === 'function' ? initial() : initial))
112
- }
113
-
114
- /** `[signal, setState]`; `setState(v | prev => next)`; the same value: no update. */
115
- function useState (initialState) {
116
- const s = useSignal(initialState)
117
- const set = React.useCallback((v) => s.set(typeof v === 'function' ? v(s.peek()) : v), [s])
118
- return [s, set]
119
- }
120
-
121
- /** `[signal, dispatch]`, with the latest render's reducer (as React does). */
122
- function useReducer (reducer, initialArg, init) {
123
- const s = useOnce(() => signal(init !== undefined ? init(initialArg) : initialArg))
124
- const latest = useLatest(reducer)
125
- const dispatch = React.useCallback((action) => s.set(latest.current(s.peek(), action)), [s])
126
- return [s, dispatch]
127
- }
128
-
129
- /** `{ current }`, observable by effects and memos (an element mounting re-runs
130
- * them); reading it while rendering doesn't subscribe, as with React's refs. */
131
- function useRef (initialValue) {
132
- return useOnce(() => new Ref(initialValue))
133
- }
134
-
135
- // ---- derived values ----------------------------------------------------------
136
-
137
- /** A memo: recomputed when the signals it reads change, and replaced (like
138
- * useMemo's value) when `deps` change; without deps, every render. */
139
- function useMemo (fn, deps) {
140
- const latest = useLatest(fn)
141
- // eslint-disable-next-line react-hooks/exhaustive-deps
142
- return React.useMemo(() => memo(() => latest.current()), deps)
143
- }
144
-
145
- /** Rust's `Memo::new`: `useMemo` with `deps = []` by default. */
146
- function useComputed (fn, deps = []) {
147
- return useMemo(fn, deps)
148
- }
149
-
150
- /** React's `useCallback`; calls are batched (their writes make one update) and
151
- * untracked (calling it in an effect doesn't subscribe to what it reads). */
152
- function useCallback (fn, deps) {
153
- // eslint-disable-next-line react-hooks/exhaustive-deps
154
- return React.useCallback((...args) => batch(() => untrack(() => fn(...args))), deps)
155
- }
156
-
157
- /** React's `useEffectEvent`: a stable function that calls the latest render's
158
- * `fn`; calls are batched and untracked (an effect calling it doesn't
159
- * subscribe to the signals `fn` reads). */
160
- function useEffectEvent (fn) {
161
- const latest = useLatest(fn)
162
- return React.useCallback((...args) => batch(() => untrack(() => latest.current(...args))), [latest])
163
- }
164
-
165
- // ---- effects -----------------------------------------------------------------
166
-
167
- function effectHook (reactHook) {
168
- return function (fn, deps) {
169
- const latest = useLatest(fn)
170
- // React's timing and deps; in between, re-runs when the signals it read change
171
- reactHook(() => effect(() => latest.current()), deps)
172
- }
173
- }
174
-
175
- /** React's `useEffect`, also re-run when the signals it read change. */
176
- const useEffect = effectHook(React.useEffect)
177
- /** React's `useLayoutEffect`, also re-run when the signals it read change. */
178
- const useLayoutEffect = effectHook(React.useLayoutEffect)
179
- /** React's `useInsertionEffect`, also re-run when the signals it read change. */
180
- const useInsertionEffect = effectHook(React.useInsertionEffect)
181
-
182
- // ---- external stores ---------------------------------------------------------
183
-
184
- /** A signal following the store. */
185
- function useSyncExternalStore (subscribe, getSnapshot) {
186
- const s = useOnce(() => signal(getSnapshot()))
187
- const latest = useLatest(getSnapshot)
188
- React.useLayoutEffect(() => {
189
- const check = () => s.set(latest.current())
190
- check() // changed between render and subscribe
191
- return subscribe(check)
192
- }, [subscribe, s])
193
- React.useLayoutEffect(() => { s.set(latest.current()) }) // a new getSnapshot
194
- return s
195
- }
196
-
197
- /** Re-render this component when `source` changes; returns its value (for
198
- * components rendered without this package's JSX runtime). */
199
- function useSignalValue (source) {
200
- const subscribe = React.useCallback((onChange) => {
201
- let first = true
202
- return effect(() => {
203
- source.value // subscribe
204
- if (first) first = false
205
- else onChange()
206
- })
207
- }, [source])
208
- return React.useSyncExternalStore(subscribe, () => source.peek())
209
- }
210
-
211
- // ---- transitions and actions -------------------------------------------------
212
-
213
- // Run `action` as a BUI transition and a React transition at once: signal
214
- // writes defer their effects, React state updates render at transition priority.
215
- // React tracks the action's promise (its optimistic updates last until it
216
- // settles) but never sees its rejection: each hook sends errors where React's does.
217
- const quiet = () => {}
218
- function transition (action) {
219
- return S.startTransition(() => {
220
- let result
221
- let failure = null
222
- React.startTransition(() => {
223
- try {
224
- result = action()
225
- } catch (error) {
226
- failure = { error } // React would report it globally and swallow it
227
- return
228
- }
229
- return result && typeof result.then === 'function' ? result.then(quiet, quiet) : undefined
230
- })
231
- if (failure !== null) throw failure.error
232
- return result
233
- })
234
- }
235
-
236
- /** React's `startTransition`, for signal writes too. Returns a promise that
237
- * settles when the action and the updates it caused are done. */
238
- function startTransition (action) {
239
- return transition(action)
240
- }
241
-
242
- /** A setter that rethrows `error` during render: React's way to reach the
243
- * nearest error boundary from an async callback. */
244
- function useThrowToBoundary () {
245
- const [, set] = React.useState(null)
246
- return React.useCallback((error) => set(() => { throw error }), [])
247
- }
248
-
249
- /** `[isPending signal, startTransition(action)]`. `action` may be async;
250
- * isPending stays true until it settles and its deferred updates have run.
251
- * An error goes to the nearest error boundary. */
252
- function useTransition () {
253
- const isPending = useOnce(() => signal(false))
254
- const fail = useThrowToBoundary()
255
- const start = React.useCallback((action) => {
256
- isPending._count = (isPending._count ?? 0) + 1
257
- S.urgent(() => isPending.set(true))
258
- const settle = () => {
259
- if (--isPending._count === 0) S.urgent(() => isPending.set(false))
260
- }
261
- let done
262
- try {
263
- done = transition(action)
264
- } catch (error) {
265
- settle()
266
- return fail(error)
267
- }
268
- done.then(settle, (error) => { settle(); fail(error) })
269
- }, [isPending, fail])
270
- return [isPending, start]
271
- }
272
-
273
- /** A signal following `value` (a signal or a value) at transition priority,
274
- * after urgent updates; `initialValue` first, if given. */
275
- function useDeferredValue (value, initialValue) {
276
- const read = () => (isSignal(value) ? value.peek() : value)
277
- const out = useOnce(() => signal(initialValue !== undefined ? initialValue : read()))
278
- const latest = useLatest(value)
279
- React.useEffect(() => {
280
- let scheduled = false
281
- let live = true
282
- const push = () => {
283
- if (scheduled) return
284
- scheduled = true
285
- S.scheduleDeferred(() => {
286
- scheduled = false
287
- if (!live) return
288
- const v = latest.current
289
- out.set(isSignal(v) ? v.peek() : v)
290
- })
291
- }
292
- if (isSignal(value)) {
293
- const stop = effect(() => { value.value; push() })
294
- return () => { live = false; stop() }
295
- }
296
- push()
297
- return () => { live = false }
298
- }, [value, out])
299
- return out
300
- }
301
-
302
- /** `[state signal, dispatch, isPending signal]`. Dispatches queue and run in
303
- * order, each with the previous state; an action may be async. A thrown
304
- * error goes to the nearest error boundary and drops the queue, as in React. */
305
- function useActionState (action, initialState) {
306
- const state = useOnce(() => signal(initialState))
307
- const isPending = useOnce(() => signal(false))
308
- const latest = useLatest(action)
309
- const queue = useOnce(() => ({ items: [], running: false }))
310
- const fail = useThrowToBoundary()
311
-
312
- const runNext = React.useCallback(() => {
313
- const payload = queue.items.shift()
314
- queue.running = true
315
- let result
316
- try {
317
- transition(() => {
318
- result = latest.current(state.peek(), payload)
319
- return result
320
- }).catch(quiet) // the error is handled below, from `result`
321
- } catch (error) {
322
- result = Promise.reject(error)
323
- }
324
- Promise.resolve(result).then((next) => {
325
- const more = queue.items.length > 0
326
- batch(() => {
327
- state.set(next)
328
- if (!more) isPending.set(false)
329
- })
330
- if (more) runNext()
331
- else queue.running = false
332
- }, (err) => {
333
- queue.items.length = 0
334
- queue.running = false
335
- isPending.set(false)
336
- fail(err)
337
- })
338
- }, [state, isPending, queue, fail])
339
-
340
- const dispatch = React.useCallback((payload) => {
341
- queue.items.push(payload)
342
- if (!queue.running) {
343
- S.urgent(() => isPending.set(true))
344
- runNext()
345
- }
346
- }, [queue, isPending, runNext])
347
-
348
- return [state, dispatch, isPending]
349
- }
350
-
351
- /** `[optimistic signal, addOptimistic]`. Updates added during a transition or
352
- * action show at once and are dropped when it ends; `passthrough` (a signal
353
- * or a value) shows through otherwise. */
354
- function useOptimistic (passthrough, reducer) {
355
- const updates = useOnce(() => signal([]))
356
- const latestReducer = useLatest(reducer)
357
- const out = React.useMemo(() => memo(() => {
358
- let v = isSignal(passthrough) ? passthrough.value : passthrough
359
- for (const u of updates.value) v = latestReducer.current ? latestReducer.current(v, u.action) : u.action
360
- return v
361
- }), [passthrough, updates])
362
- const add = React.useCallback((action) => {
363
- const entry = { action }
364
- const remove = () => S.urgent(() => updates.set(updates.peek().filter((e) => e !== entry)))
365
- S.urgent(() => updates.set([...updates.peek(), entry]))
366
- if (!S.onTransitionDone(remove)) {
367
- console.error('An optimistic state update occurred outside a transition or action. ' +
368
- 'To fix, move the update to an action, or wrap it in startTransition.')
369
- setTimeout(remove, 0)
370
- }
371
- }, [updates])
372
- return [out, add]
373
- }
374
-
375
- module.exports = {
376
- NO_TWIN,
377
- track,
378
- createElement,
379
- startTransition,
380
- useSignal,
381
- useComputed,
382
- useSignalValue,
383
- useState,
384
- useReducer,
385
- useRef,
386
- useMemo,
387
- useCallback,
388
- useEffectEvent,
389
- useEffect,
390
- useLayoutEffect,
391
- useInsertionEffect,
392
- useSyncExternalStore,
393
- useTransition,
394
- useDeferredValue,
395
- useActionState,
396
- useOptimistic,
397
- }
1
+ // React's hooks, by React's names, with signals for state.
2
+ //
3
+ // `useState`, `useReducer`, `useMemo` ... take React's arguments, run at
4
+ // React's moments and return React's shapes, but the state they hold is a
5
+ // signal. Pass it as a prop or child (`<span>{count}</span>`) and a change
6
+ // updates that one node: the component doesn't re-render. Read it while
7
+ // rendering (`count.value`, `count + 1`, a condition) and the component
8
+ // re-renders when it changes, exactly as with React's own hooks.
9
+ //
10
+ // The second part needs to know which component is rendering: components
11
+ // created through this package's JSX runtime (`jsxImportSource:
12
+ // "@blender-ui/react"`) or `createElement` are wrapped with `track()`.
13
+ //
14
+ // Built only on React's public hooks (useState, useReducer, useRef, useMemo,
15
+ // the effect hooks, startTransition), so React's semantics (StrictMode,
16
+ // concurrent rendering) come from React itself. test/hooks.test.js runs each
17
+ // hook beside React's original and fails when React adds a hook that has
18
+ // neither a signal version here nor an entry in NO_TWIN.
19
+ //
20
+ // SPDX-License-Identifier: GPL-2.0-or-later
21
+ 'use strict'
22
+
23
+ const React = require('react')
24
+ const S = require('@blender-ui/native')
25
+ const { signal, memo, effect, batch, untrack, isSignal, Tracker, Ref } = S
26
+
27
+ /** React hooks re-exported unchanged, and why (the coverage test reads this). */
28
+ const NO_TWIN = {
29
+ use: 'suspends rendering: inherently a render-time read',
30
+ useContext: 'pass a signal as the context value: consumers bind to it without re-rendering',
31
+ useDebugValue: 'no state',
32
+ useId: 'no state',
33
+ useImperativeHandle: 'no state (works with useRef)',
34
+ }
35
+
36
+ // The latest value, for callbacks that outlive the render that made them.
37
+ function useLatest (value) {
38
+ const ref = React.useRef(value)
39
+ ref.current = value
40
+ return ref
41
+ }
42
+
43
+ /** A component-owned value, created once. */
44
+ function useOnce (create) {
45
+ return React.useState(create)[0]
46
+ }
47
+
48
+ // ---- render tracking -----------------------------------------------------------
49
+
50
+ const bump = (n) => n + 1
51
+
52
+ /** Re-render this component when a signal its render read changes. */
53
+ function useRenderTracker () {
54
+ const [, force] = React.useReducer(bump, 0)
55
+ const tracker = useOnce(() => new Tracker(() => force()))
56
+ const reads = React.useRef(null)
57
+ // after each commit: follow what that render read
58
+ React.useLayoutEffect(() => { tracker.commit(reads.current) })
59
+ // mount / unmount (StrictMode re-mounts: pause, then resume)
60
+ React.useLayoutEffect(() => {
61
+ tracker.resume()
62
+ return () => tracker.pause()
63
+ }, [tracker])
64
+ return { tracker, reads }
65
+ }
66
+
67
+ const tracked = new WeakMap()
68
+ const MEMO = Symbol.for('react.memo')
69
+ const FORWARD_REF = Symbol.for('react.forward_ref')
70
+
71
+ /** `type` with render tracking: function components, `memo` and `forwardRef`. */
72
+ function track (type) {
73
+ if (type == null) return type
74
+ if (typeof type === 'function') {
75
+ if (type.prototype?.isReactComponent) return type // class components: no hooks
76
+ } else if (typeof type !== 'object' || (type.$$typeof !== MEMO && type.$$typeof !== FORWARD_REF)) {
77
+ return type // host components, fragments, providers, lazy ...
78
+ }
79
+ let t = tracked.get(type)
80
+ if (t !== undefined) return t
81
+ if (typeof type === 'function') {
82
+ t = function (props, secondArg) {
83
+ const { tracker, reads } = useRenderTracker()
84
+ const [result, r] = tracker.run(() => type(props, secondArg))
85
+ reads.current = r
86
+ return result
87
+ }
88
+ Object.defineProperty(t, 'name', { value: type.name })
89
+ t.displayName = type.displayName || type.name
90
+ } else if (type.$$typeof === MEMO) {
91
+ t = React.memo(track(type.type), type.compare)
92
+ t.displayName = type.displayName
93
+ } else {
94
+ t = React.forwardRef(track(type.render))
95
+ t.displayName = type.displayName
96
+ }
97
+ tracked.set(type, t)
98
+ tracked.set(t, t)
99
+ return t
100
+ }
101
+
102
+ /** React's `createElement`, with render tracking for components. */
103
+ function createElement (type, props, ...children) {
104
+ return React.createElement(track(type), props, ...children)
105
+ }
106
+
107
+ // ---- state -----------------------------------------------------------------
108
+
109
+ /** Rust's `use_signal`: a signal owned by this component. */
110
+ function useSignal (initial) {
111
+ return useOnce(() => signal(typeof initial === 'function' ? initial() : initial))
112
+ }
113
+
114
+ /** `[signal, setState]`; `setState(v | prev => next)`; the same value: no update. */
115
+ function useState (initialState) {
116
+ const s = useSignal(initialState)
117
+ const set = React.useCallback((v) => s.set(typeof v === 'function' ? v(s.peek()) : v), [s])
118
+ return [s, set]
119
+ }
120
+
121
+ /** `[signal, dispatch]`, with the latest render's reducer (as React does). */
122
+ function useReducer (reducer, initialArg, init) {
123
+ const s = useOnce(() => signal(init !== undefined ? init(initialArg) : initialArg))
124
+ const latest = useLatest(reducer)
125
+ const dispatch = React.useCallback((action) => s.set(latest.current(s.peek(), action)), [s])
126
+ return [s, dispatch]
127
+ }
128
+
129
+ /** `{ current }`, observable by effects and memos (an element mounting re-runs
130
+ * them); reading it while rendering doesn't subscribe, as with React's refs. */
131
+ function useRef (initialValue) {
132
+ return useOnce(() => new Ref(initialValue))
133
+ }
134
+
135
+ // ---- derived values ----------------------------------------------------------
136
+
137
+ /** A memo: recomputed when the signals it reads change, and replaced (like
138
+ * useMemo's value) when `deps` change; without deps, every render. */
139
+ function useMemo (fn, deps) {
140
+ const latest = useLatest(fn)
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ return React.useMemo(() => memo(() => latest.current()), deps)
143
+ }
144
+
145
+ /** Rust's `Memo::new`: `useMemo` with `deps = []` by default. */
146
+ function useComputed (fn, deps = []) {
147
+ return useMemo(fn, deps)
148
+ }
149
+
150
+ /** React's `useCallback`; calls are batched (their writes make one update) and
151
+ * untracked (calling it in an effect doesn't subscribe to what it reads). */
152
+ function useCallback (fn, deps) {
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ return React.useCallback((...args) => batch(() => untrack(() => fn(...args))), deps)
155
+ }
156
+
157
+ /** React's `useEffectEvent`: a stable function that calls the latest render's
158
+ * `fn`; calls are batched and untracked (an effect calling it doesn't
159
+ * subscribe to the signals `fn` reads). */
160
+ function useEffectEvent (fn) {
161
+ const latest = useLatest(fn)
162
+ return React.useCallback((...args) => batch(() => untrack(() => latest.current(...args))), [latest])
163
+ }
164
+
165
+ // ---- effects -----------------------------------------------------------------
166
+
167
+ function effectHook (reactHook) {
168
+ return function (fn, deps) {
169
+ const latest = useLatest(fn)
170
+ // React's timing and deps; in between, re-runs when the signals it read change
171
+ reactHook(() => effect(() => latest.current()), deps)
172
+ }
173
+ }
174
+
175
+ /** React's `useEffect`, also re-run when the signals it read change. */
176
+ const useEffect = effectHook(React.useEffect)
177
+ /** React's `useLayoutEffect`, also re-run when the signals it read change. */
178
+ const useLayoutEffect = effectHook(React.useLayoutEffect)
179
+ /** React's `useInsertionEffect`, also re-run when the signals it read change. */
180
+ const useInsertionEffect = effectHook(React.useInsertionEffect)
181
+
182
+ // ---- external stores ---------------------------------------------------------
183
+
184
+ /** A signal following the store. */
185
+ function useSyncExternalStore (subscribe, getSnapshot) {
186
+ const s = useOnce(() => signal(getSnapshot()))
187
+ const latest = useLatest(getSnapshot)
188
+ React.useLayoutEffect(() => {
189
+ const check = () => s.set(latest.current())
190
+ check() // changed between render and subscribe
191
+ return subscribe(check)
192
+ }, [subscribe, s])
193
+ React.useLayoutEffect(() => { s.set(latest.current()) }) // a new getSnapshot
194
+ return s
195
+ }
196
+
197
+ /** Re-render this component when `source` changes; returns its value (for
198
+ * components rendered without this package's JSX runtime). */
199
+ function useSignalValue (source) {
200
+ const subscribe = React.useCallback((onChange) => {
201
+ let first = true
202
+ return effect(() => {
203
+ source.value // subscribe
204
+ if (first) first = false
205
+ else onChange()
206
+ })
207
+ }, [source])
208
+ return React.useSyncExternalStore(subscribe, () => source.peek())
209
+ }
210
+
211
+ // ---- transitions and actions -------------------------------------------------
212
+
213
+ // Run `action` as a BUI transition and a React transition at once: signal
214
+ // writes defer their effects, React state updates render at transition priority.
215
+ // React tracks the action's promise (its optimistic updates last until it
216
+ // settles) but never sees its rejection: each hook sends errors where React's does.
217
+ const quiet = () => {}
218
+ function transition (action) {
219
+ return S.startTransition(() => {
220
+ let result
221
+ let failure = null
222
+ React.startTransition(() => {
223
+ try {
224
+ result = action()
225
+ } catch (error) {
226
+ failure = { error } // React would report it globally and swallow it
227
+ return
228
+ }
229
+ return result && typeof result.then === 'function' ? result.then(quiet, quiet) : undefined
230
+ })
231
+ if (failure !== null) throw failure.error
232
+ return result
233
+ })
234
+ }
235
+
236
+ /** React's `startTransition`, for signal writes too. Returns a promise that
237
+ * settles when the action and the updates it caused are done. */
238
+ function startTransition (action) {
239
+ return transition(action)
240
+ }
241
+
242
+ /** A setter that rethrows `error` during render: React's way to reach the
243
+ * nearest error boundary from an async callback. */
244
+ function useThrowToBoundary () {
245
+ const [, set] = React.useState(null)
246
+ return React.useCallback((error) => set(() => { throw error }), [])
247
+ }
248
+
249
+ /** `[isPending signal, startTransition(action)]`. `action` may be async;
250
+ * isPending stays true until it settles and its deferred updates have run.
251
+ * An error goes to the nearest error boundary. */
252
+ function useTransition () {
253
+ const isPending = useOnce(() => signal(false))
254
+ const fail = useThrowToBoundary()
255
+ const start = React.useCallback((action) => {
256
+ isPending._count = (isPending._count ?? 0) + 1
257
+ S.urgent(() => isPending.set(true))
258
+ const settle = () => {
259
+ if (--isPending._count === 0) S.urgent(() => isPending.set(false))
260
+ }
261
+ let done
262
+ try {
263
+ done = transition(action)
264
+ } catch (error) {
265
+ settle()
266
+ return fail(error)
267
+ }
268
+ done.then(settle, (error) => { settle(); fail(error) })
269
+ }, [isPending, fail])
270
+ return [isPending, start]
271
+ }
272
+
273
+ /** A signal following `value` (a signal or a value) at transition priority,
274
+ * after urgent updates; `initialValue` first, if given. */
275
+ function useDeferredValue (value, initialValue) {
276
+ const read = () => (isSignal(value) ? value.peek() : value)
277
+ const out = useOnce(() => signal(initialValue !== undefined ? initialValue : read()))
278
+ const latest = useLatest(value)
279
+ React.useEffect(() => {
280
+ let scheduled = false
281
+ let live = true
282
+ const push = () => {
283
+ if (scheduled) return
284
+ scheduled = true
285
+ S.scheduleDeferred(() => {
286
+ scheduled = false
287
+ if (!live) return
288
+ const v = latest.current
289
+ out.set(isSignal(v) ? v.peek() : v)
290
+ })
291
+ }
292
+ if (isSignal(value)) {
293
+ const stop = effect(() => { value.value; push() })
294
+ return () => { live = false; stop() }
295
+ }
296
+ push()
297
+ return () => { live = false }
298
+ }, [value, out])
299
+ return out
300
+ }
301
+
302
+ /** `[state signal, dispatch, isPending signal]`. Dispatches queue and run in
303
+ * order, each with the previous state; an action may be async. A thrown
304
+ * error goes to the nearest error boundary and drops the queue, as in React. */
305
+ function useActionState (action, initialState) {
306
+ const state = useOnce(() => signal(initialState))
307
+ const isPending = useOnce(() => signal(false))
308
+ const latest = useLatest(action)
309
+ const queue = useOnce(() => ({ items: [], running: false }))
310
+ const fail = useThrowToBoundary()
311
+
312
+ const runNext = React.useCallback(() => {
313
+ const payload = queue.items.shift()
314
+ queue.running = true
315
+ let result
316
+ try {
317
+ transition(() => {
318
+ result = latest.current(state.peek(), payload)
319
+ return result
320
+ }).catch(quiet) // the error is handled below, from `result`
321
+ } catch (error) {
322
+ result = Promise.reject(error)
323
+ }
324
+ Promise.resolve(result).then((next) => {
325
+ const more = queue.items.length > 0
326
+ batch(() => {
327
+ state.set(next)
328
+ if (!more) isPending.set(false)
329
+ })
330
+ if (more) runNext()
331
+ else queue.running = false
332
+ }, (err) => {
333
+ queue.items.length = 0
334
+ queue.running = false
335
+ isPending.set(false)
336
+ fail(err)
337
+ })
338
+ }, [state, isPending, queue, fail])
339
+
340
+ const dispatch = React.useCallback((payload) => {
341
+ queue.items.push(payload)
342
+ if (!queue.running) {
343
+ S.urgent(() => isPending.set(true))
344
+ runNext()
345
+ }
346
+ }, [queue, isPending, runNext])
347
+
348
+ return [state, dispatch, isPending]
349
+ }
350
+
351
+ /** `[optimistic signal, addOptimistic]`. Updates added during a transition or
352
+ * action show at once and are dropped when it ends; `passthrough` (a signal
353
+ * or a value) shows through otherwise. */
354
+ function useOptimistic (passthrough, reducer) {
355
+ const updates = useOnce(() => signal([]))
356
+ const latestReducer = useLatest(reducer)
357
+ const out = React.useMemo(() => memo(() => {
358
+ let v = isSignal(passthrough) ? passthrough.value : passthrough
359
+ for (const u of updates.value) v = latestReducer.current ? latestReducer.current(v, u.action) : u.action
360
+ return v
361
+ }), [passthrough, updates])
362
+ const add = React.useCallback((action) => {
363
+ const entry = { action }
364
+ const remove = () => S.urgent(() => updates.set(updates.peek().filter((e) => e !== entry)))
365
+ S.urgent(() => updates.set([...updates.peek(), entry]))
366
+ if (!S.onTransitionDone(remove)) {
367
+ console.error('An optimistic state update occurred outside a transition or action. ' +
368
+ 'To fix, move the update to an action, or wrap it in startTransition.')
369
+ setTimeout(remove, 0)
370
+ }
371
+ }, [updates])
372
+ return [out, add]
373
+ }
374
+
375
+ module.exports = {
376
+ NO_TWIN,
377
+ track,
378
+ createElement,
379
+ startTransition,
380
+ useSignal,
381
+ useComputed,
382
+ useSignalValue,
383
+ useState,
384
+ useReducer,
385
+ useRef,
386
+ useMemo,
387
+ useCallback,
388
+ useEffectEvent,
389
+ useEffect,
390
+ useLayoutEffect,
391
+ useInsertionEffect,
392
+ useSyncExternalStore,
393
+ useTransition,
394
+ useDeferredValue,
395
+ useActionState,
396
+ useOptimistic,
397
+ }