@preact/signals-react 1.3.6 → 1.3.8
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/CHANGELOG.md +17 -0
- package/dist/signals.d.ts +1 -1
- package/dist/signals.js +1 -1
- package/dist/signals.js.map +1 -1
- package/dist/signals.min.js +1 -1
- package/dist/signals.min.js.map +1 -1
- package/dist/signals.mjs +1 -1
- package/dist/signals.mjs.map +1 -1
- package/dist/signals.module.js +1 -1
- package/dist/signals.module.js.map +1 -1
- package/package.json +12 -2
- package/runtime/dist/auto.d.ts +1 -0
- package/runtime/dist/index.d.ts +40 -4
- package/runtime/dist/runtime.js +1 -1
- package/runtime/dist/runtime.js.map +1 -1
- package/runtime/dist/runtime.min.js +1 -1
- package/runtime/dist/runtime.min.js.map +1 -1
- package/runtime/dist/runtime.mjs +1 -1
- package/runtime/dist/runtime.mjs.map +1 -1
- package/runtime/dist/runtime.module.js +1 -1
- package/runtime/dist/runtime.module.js.map +1 -1
- package/runtime/src/auto.ts +36 -15
- package/runtime/src/index.ts +238 -33
- package/src/index.ts +6 -2
- package/runtime/test/useSignals.test.tsx +0 -422
- package/test/browser/exports.test.tsx +0 -18
- package/test/browser/mounts.test.tsx +0 -32
- package/test/browser/react-router.test.tsx +0 -63
- package/test/browser/updates.test.tsx +0 -735
- package/test/node/renderToStaticMarkup.test.tsx +0 -22
- package/test/shared/mounting.tsx +0 -184
- package/test/shared/utils.ts +0 -127
package/runtime/src/index.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
signal,
|
|
3
|
+
computed,
|
|
4
|
+
effect,
|
|
5
|
+
Signal,
|
|
6
|
+
ReadonlySignal,
|
|
7
|
+
} from "@preact/signals-core";
|
|
2
8
|
import { useRef, useMemo, useEffect } from "react";
|
|
3
9
|
import { useSyncExternalStore } from "use-sync-external-store/shim/index.js";
|
|
10
|
+
import { isAutoSignalTrackingInstalled } from "./auto";
|
|
4
11
|
|
|
5
12
|
export { installAutoSignalTracking } from "./auto";
|
|
6
13
|
|
|
7
14
|
const Empty = [] as const;
|
|
8
15
|
const ReactElemType = Symbol.for("react.element"); // https://github.com/facebook/react/blob/346c7d4c43a0717302d446da9e7423a8e28d8996/packages/shared/ReactSymbols.js#L15
|
|
16
|
+
const noop = () => {};
|
|
9
17
|
|
|
10
18
|
export function wrapJsx<T>(jsx: T): T {
|
|
11
19
|
if (typeof jsx !== "function") return jsx;
|
|
@@ -24,7 +32,8 @@ export function wrapJsx<T>(jsx: T): T {
|
|
|
24
32
|
} as any as T;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
const symDispose: unique symbol =
|
|
35
|
+
const symDispose: unique symbol =
|
|
36
|
+
(Symbol as any).dispose || Symbol.for("Symbol.dispose");
|
|
28
37
|
|
|
29
38
|
interface Effect {
|
|
30
39
|
_sources: object | undefined;
|
|
@@ -33,41 +42,99 @@ interface Effect {
|
|
|
33
42
|
_dispose(): void;
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Use this flag to represent a bare `useSignals` call that doesn't manually
|
|
47
|
+
* close its effect store and relies on auto-closing when the next useSignals is
|
|
48
|
+
* called or after a microtask
|
|
49
|
+
*/
|
|
50
|
+
const UNMANAGED = 0;
|
|
51
|
+
/**
|
|
52
|
+
* Use this flag to represent a `useSignals` call that is manually closed by a
|
|
53
|
+
* try/finally block in a component's render method. This is the default usage
|
|
54
|
+
* that the react-transform plugin uses.
|
|
55
|
+
*/
|
|
56
|
+
const MANAGED_COMPONENT = 1;
|
|
57
|
+
/**
|
|
58
|
+
* Use this flag to represent a `useSignals` call that is manually closed by a
|
|
59
|
+
* try/finally block in a hook body. This is the default usage that the
|
|
60
|
+
* react-transform plugin uses.
|
|
61
|
+
*/
|
|
62
|
+
const MANAGED_HOOK = 2;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* An enum defining how this store is used. See the documentation for each enum
|
|
66
|
+
* member for more details.
|
|
67
|
+
* @see {@link UNMANAGED}
|
|
68
|
+
* @see {@link MANAGED_COMPONENT}
|
|
69
|
+
* @see {@link MANAGED_HOOK}
|
|
70
|
+
*/
|
|
71
|
+
type EffectStoreUsage =
|
|
72
|
+
| typeof UNMANAGED
|
|
73
|
+
| typeof MANAGED_COMPONENT
|
|
74
|
+
| typeof MANAGED_HOOK;
|
|
75
|
+
|
|
36
76
|
export interface EffectStore {
|
|
37
|
-
|
|
77
|
+
/**
|
|
78
|
+
* An enum defining how this hook is used and whether it is invoked in a
|
|
79
|
+
* component's body or hook body. See the comment on `EffectStoreUsage` for
|
|
80
|
+
* more details.
|
|
81
|
+
*/
|
|
82
|
+
readonly _usage: EffectStoreUsage;
|
|
83
|
+
readonly effect: Effect;
|
|
38
84
|
subscribe(onStoreChange: () => void): () => void;
|
|
39
85
|
getSnapshot(): number;
|
|
86
|
+
/** startEffect - begin tracking signals used in this component */
|
|
87
|
+
_start(): void;
|
|
40
88
|
/** finishEffect - stop tracking the signals used in this component */
|
|
41
89
|
f(): void;
|
|
42
90
|
[symDispose](): void;
|
|
43
91
|
}
|
|
44
92
|
|
|
45
|
-
let
|
|
93
|
+
let currentStore: EffectStore | undefined;
|
|
94
|
+
|
|
95
|
+
function startComponentEffect(
|
|
96
|
+
prevStore: EffectStore | undefined,
|
|
97
|
+
nextStore: EffectStore
|
|
98
|
+
) {
|
|
99
|
+
const endEffect = nextStore.effect._start();
|
|
100
|
+
currentStore = nextStore;
|
|
46
101
|
|
|
47
|
-
|
|
48
|
-
// end tracking for the current update:
|
|
49
|
-
if (finishUpdate) finishUpdate();
|
|
50
|
-
// start tracking the new update:
|
|
51
|
-
finishUpdate = store && store.effect._start();
|
|
102
|
+
return finishComponentEffect.bind(nextStore, prevStore, endEffect);
|
|
52
103
|
}
|
|
53
104
|
|
|
54
|
-
|
|
105
|
+
function finishComponentEffect(
|
|
106
|
+
this: EffectStore,
|
|
107
|
+
prevStore: EffectStore | undefined,
|
|
108
|
+
endEffect: () => void
|
|
109
|
+
) {
|
|
110
|
+
endEffect();
|
|
111
|
+
currentStore = prevStore;
|
|
112
|
+
}
|
|
55
113
|
|
|
56
114
|
/**
|
|
57
|
-
* A redux-like store whose store value is a positive 32bit integer (a
|
|
115
|
+
* A redux-like store whose store value is a positive 32bit integer (a
|
|
116
|
+
* 'version').
|
|
58
117
|
*
|
|
59
118
|
* React subscribes to this store and gets a snapshot of the current 'version',
|
|
60
|
-
* whenever the 'version' changes, we tell React it's time to update the
|
|
119
|
+
* whenever the 'version' changes, we tell React it's time to update the
|
|
120
|
+
* component (call 'onStoreChange').
|
|
61
121
|
*
|
|
62
|
-
* How we achieve this is by creating a binding with an 'effect', when the
|
|
63
|
-
* we update our store version and tell React to
|
|
122
|
+
* How we achieve this is by creating a binding with an 'effect', when the
|
|
123
|
+
* `effect._callback' is called, we update our store version and tell React to
|
|
124
|
+
* re-render the component ([1] We don't really care when/how React does it).
|
|
64
125
|
*
|
|
65
126
|
* [1]
|
|
66
127
|
* @see https://react.dev/reference/react/useSyncExternalStore
|
|
67
|
-
* @see
|
|
128
|
+
* @see
|
|
129
|
+
* https://github.com/reactjs/rfcs/blob/main/text/0214-use-sync-external-store.md
|
|
130
|
+
*
|
|
131
|
+
* @param _usage An enum defining how this hook is used and whether it is
|
|
132
|
+
* invoked in a component's body or hook body. See the comment on
|
|
133
|
+
* `EffectStoreUsage` for more details.
|
|
68
134
|
*/
|
|
69
|
-
function createEffectStore(): EffectStore {
|
|
135
|
+
function createEffectStore(_usage: EffectStoreUsage): EffectStore {
|
|
70
136
|
let effectInstance!: Effect;
|
|
137
|
+
let endEffect: (() => void) | undefined;
|
|
71
138
|
let version = 0;
|
|
72
139
|
let onChangeNotifyReact: (() => void) | undefined;
|
|
73
140
|
|
|
@@ -80,6 +147,7 @@ function createEffectStore(): EffectStore {
|
|
|
80
147
|
};
|
|
81
148
|
|
|
82
149
|
return {
|
|
150
|
+
_usage,
|
|
83
151
|
effect: effectInstance,
|
|
84
152
|
subscribe(onStoreChange) {
|
|
85
153
|
onChangeNotifyReact = onStoreChange;
|
|
@@ -103,39 +171,166 @@ function createEffectStore(): EffectStore {
|
|
|
103
171
|
getSnapshot() {
|
|
104
172
|
return version;
|
|
105
173
|
},
|
|
174
|
+
_start() {
|
|
175
|
+
// In general, we want to support two kinds of usages of useSignals:
|
|
176
|
+
//
|
|
177
|
+
// A) Managed: calling useSignals in a component or hook body wrapped in a
|
|
178
|
+
// try/finally (like what the react-transform plugin does)
|
|
179
|
+
//
|
|
180
|
+
// B) Unmanaged: Calling useSignals directly without wrapping in a
|
|
181
|
+
// try/finally
|
|
182
|
+
//
|
|
183
|
+
// For managed, we finish the effect in the finally block of the component
|
|
184
|
+
// or hook body. For unmanaged, we finish the effect in the next
|
|
185
|
+
// useSignals call or after a microtask.
|
|
186
|
+
//
|
|
187
|
+
// There are different tradeoffs which each approach. With managed, using
|
|
188
|
+
// a try/finally ensures that only signals used in the component or hook
|
|
189
|
+
// body are tracked. However, signals accessed in render props are missed
|
|
190
|
+
// because the render prop is invoked in another component that may or may
|
|
191
|
+
// not realize it is rendering signals accessed in the render prop it is
|
|
192
|
+
// given.
|
|
193
|
+
//
|
|
194
|
+
// The other approach is "unmanaged": to call useSignals directly without
|
|
195
|
+
// wrapping in a try/finally. This approach is easier to manually write in
|
|
196
|
+
// situations where a build step isn't available but does open up the
|
|
197
|
+
// possibility of catching signals accessed in other code before the
|
|
198
|
+
// effect is closed (e.g. in a layout effect). Most situations where this
|
|
199
|
+
// could happen are generally consider bad patterns or bugs. For example,
|
|
200
|
+
// using a signal in a component and not having a call to `useSignals`
|
|
201
|
+
// would be an bug. Or using a signal in `useLayoutEffect` is generally
|
|
202
|
+
// not recommended since that layout effect won't update when the signals'
|
|
203
|
+
// value change.
|
|
204
|
+
//
|
|
205
|
+
// To support both approaches, we need to track how each invocation of
|
|
206
|
+
// useSignals is used, so we can properly transition between different
|
|
207
|
+
// kinds of usages.
|
|
208
|
+
//
|
|
209
|
+
// The following table shows the different scenarios and how we should
|
|
210
|
+
// handle them.
|
|
211
|
+
//
|
|
212
|
+
// Key:
|
|
213
|
+
// 0 = UNMANAGED
|
|
214
|
+
// 1 = MANAGED_COMPONENT
|
|
215
|
+
// 2 = MANAGED_HOOK
|
|
216
|
+
//
|
|
217
|
+
// Pattern:
|
|
218
|
+
// prev store usage -> this store usage: action to take
|
|
219
|
+
//
|
|
220
|
+
// - 0 -> 0: finish previous effect (unknown to unknown)
|
|
221
|
+
//
|
|
222
|
+
// We don't know how the previous effect was used, so we need to finish
|
|
223
|
+
// it before starting the next effect.
|
|
224
|
+
//
|
|
225
|
+
// - 0 -> 1: finish previous effect
|
|
226
|
+
//
|
|
227
|
+
// Assume previous invocation was another component or hook from another
|
|
228
|
+
// component. Nested component renders (renderToStaticMarkup within a
|
|
229
|
+
// component's render) won't be supported with bare useSignals calls.
|
|
230
|
+
//
|
|
231
|
+
// - 0 -> 2: capture & restore
|
|
232
|
+
//
|
|
233
|
+
// Previous invocation could be a component or a hook. Either way,
|
|
234
|
+
// restore it after our invocation so that it can continue to capture
|
|
235
|
+
// any signals after we exit.
|
|
236
|
+
//
|
|
237
|
+
// - 1 -> 0: Do nothing. Signals already captured by current effect store
|
|
238
|
+
// - 1 -> 1: capture & restore (e.g. component calls renderToStaticMarkup)
|
|
239
|
+
// - 1 -> 2: capture & restore (e.g. hook)
|
|
240
|
+
//
|
|
241
|
+
// - 2 -> 0: Do nothing. Signals already captured by current effect store
|
|
242
|
+
// - 2 -> 1: capture & restore (e.g. hook calls renderToStaticMarkup)
|
|
243
|
+
// - 2 -> 2: capture & restore (e.g. nested hook calls)
|
|
244
|
+
|
|
245
|
+
if (currentStore == undefined) {
|
|
246
|
+
endEffect = startComponentEffect(undefined, this);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const prevUsage = currentStore._usage;
|
|
251
|
+
const thisUsage = this._usage;
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
(prevUsage == UNMANAGED && thisUsage == UNMANAGED) || // 0 -> 0
|
|
255
|
+
(prevUsage == UNMANAGED && thisUsage == MANAGED_COMPONENT) // 0 -> 1
|
|
256
|
+
) {
|
|
257
|
+
// finish previous effect
|
|
258
|
+
currentStore.f();
|
|
259
|
+
endEffect = startComponentEffect(undefined, this);
|
|
260
|
+
} else if (
|
|
261
|
+
(prevUsage == MANAGED_COMPONENT && thisUsage == UNMANAGED) || // 1 -> 0
|
|
262
|
+
(prevUsage == MANAGED_HOOK && thisUsage == UNMANAGED) // 2 -> 0
|
|
263
|
+
) {
|
|
264
|
+
// Do nothing since it'll be captured by current effect store
|
|
265
|
+
} else {
|
|
266
|
+
// nested scenarios, so capture and restore the previous effect store
|
|
267
|
+
endEffect = startComponentEffect(currentStore, this);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
106
270
|
f() {
|
|
107
|
-
|
|
271
|
+
endEffect?.();
|
|
272
|
+
endEffect = undefined;
|
|
108
273
|
},
|
|
109
274
|
[symDispose]() {
|
|
110
|
-
|
|
111
|
-
}
|
|
275
|
+
this.f();
|
|
276
|
+
},
|
|
112
277
|
};
|
|
113
278
|
}
|
|
114
279
|
|
|
115
|
-
|
|
280
|
+
function createEmptyEffectStore(): EffectStore {
|
|
281
|
+
return {
|
|
282
|
+
_usage: UNMANAGED,
|
|
283
|
+
effect: {
|
|
284
|
+
_sources: undefined,
|
|
285
|
+
_callback() {},
|
|
286
|
+
_start() {
|
|
287
|
+
return noop;
|
|
288
|
+
},
|
|
289
|
+
_dispose() {},
|
|
290
|
+
},
|
|
291
|
+
subscribe() {
|
|
292
|
+
return noop;
|
|
293
|
+
},
|
|
294
|
+
getSnapshot() {
|
|
295
|
+
return 0;
|
|
296
|
+
},
|
|
297
|
+
_start() {},
|
|
298
|
+
f() {},
|
|
299
|
+
[symDispose]() {},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const emptyEffectStore = createEmptyEffectStore();
|
|
304
|
+
|
|
116
305
|
const _queueMicroTask = Promise.prototype.then.bind(Promise.resolve());
|
|
117
306
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* subscribe to changes to rerender the component when the signals change.
|
|
121
|
-
*/
|
|
122
|
-
export function useSignals(): EffectStore {
|
|
123
|
-
clearCurrentStore();
|
|
307
|
+
let finalCleanup: Promise<void> | undefined;
|
|
308
|
+
export function ensureFinalCleanup() {
|
|
124
309
|
if (!finalCleanup) {
|
|
125
310
|
finalCleanup = _queueMicroTask(() => {
|
|
126
311
|
finalCleanup = undefined;
|
|
127
|
-
|
|
312
|
+
currentStore?.f();
|
|
128
313
|
});
|
|
129
314
|
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Custom hook to create the effect to track signals used during render and
|
|
319
|
+
* subscribe to changes to rerender the component when the signals change.
|
|
320
|
+
*/
|
|
321
|
+
export function _useSignalsImplementation(
|
|
322
|
+
_usage: EffectStoreUsage = UNMANAGED
|
|
323
|
+
): EffectStore {
|
|
324
|
+
ensureFinalCleanup();
|
|
130
325
|
|
|
131
326
|
const storeRef = useRef<EffectStore>();
|
|
132
327
|
if (storeRef.current == null) {
|
|
133
|
-
storeRef.current = createEffectStore();
|
|
328
|
+
storeRef.current = createEffectStore(_usage);
|
|
134
329
|
}
|
|
135
330
|
|
|
136
331
|
const store = storeRef.current;
|
|
137
332
|
useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
138
|
-
|
|
333
|
+
store._start();
|
|
139
334
|
|
|
140
335
|
return store;
|
|
141
336
|
}
|
|
@@ -144,7 +339,12 @@ export function useSignals(): EffectStore {
|
|
|
144
339
|
* A wrapper component that renders a Signal's value directly as a Text node or JSX.
|
|
145
340
|
*/
|
|
146
341
|
function SignalValue({ data }: { data: Signal }) {
|
|
147
|
-
|
|
342
|
+
const store = _useSignalsImplementation(1);
|
|
343
|
+
try {
|
|
344
|
+
return data.value;
|
|
345
|
+
} finally {
|
|
346
|
+
store.f();
|
|
347
|
+
}
|
|
148
348
|
}
|
|
149
349
|
|
|
150
350
|
// Decorate Signals so React renders them as <SignalValue> components.
|
|
@@ -160,17 +360,22 @@ Object.defineProperties(Signal.prototype, {
|
|
|
160
360
|
ref: { configurable: true, value: null },
|
|
161
361
|
});
|
|
162
362
|
|
|
163
|
-
export function
|
|
363
|
+
export function useSignals(usage?: EffectStoreUsage): EffectStore {
|
|
364
|
+
if (isAutoSignalTrackingInstalled) return emptyEffectStore;
|
|
365
|
+
return _useSignalsImplementation(usage);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function useSignal<T>(value: T): Signal<T> {
|
|
164
369
|
return useMemo(() => signal<T>(value), Empty);
|
|
165
370
|
}
|
|
166
371
|
|
|
167
|
-
export function useComputed<T>(compute: () => T) {
|
|
372
|
+
export function useComputed<T>(compute: () => T): ReadonlySignal<T> {
|
|
168
373
|
const $compute = useRef(compute);
|
|
169
374
|
$compute.current = compute;
|
|
170
375
|
return useMemo(() => computed<T>(() => $compute.current()), Empty);
|
|
171
376
|
}
|
|
172
377
|
|
|
173
|
-
export function useSignalEffect(cb: () => void | (() => void)) {
|
|
378
|
+
export function useSignalEffect(cb: () => void | (() => void)): void {
|
|
174
379
|
const callback = useRef(cb);
|
|
175
380
|
callback.current = cb;
|
|
176
381
|
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
untracked,
|
|
9
9
|
} from "@preact/signals-core";
|
|
10
10
|
import type { ReactElement } from "react";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
useSignal,
|
|
13
|
+
useComputed,
|
|
14
|
+
useSignalEffect,
|
|
15
|
+
installAutoSignalTracking,
|
|
16
|
+
} from "@preact/signals-react/runtime"; // TODO: This duplicates runtime code between main and sub runtime packages
|
|
13
17
|
|
|
14
18
|
export {
|
|
15
19
|
signal,
|