@preact/signals-react 1.3.7 → 2.0.0
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 +21 -0
- package/README.md +66 -3
- package/auto/dist/auto.js +1 -0
- package/auto/dist/auto.js.map +1 -0
- package/auto/dist/auto.min.js +1 -0
- package/auto/dist/auto.min.js.map +1 -0
- package/auto/dist/auto.mjs +1 -0
- package/auto/dist/auto.mjs.map +1 -0
- package/auto/dist/auto.module.js +1 -0
- package/auto/dist/auto.module.js.map +1 -0
- package/auto/dist/index.d.ts +1 -0
- package/auto/package.json +25 -0
- package/auto/src/index.ts +2 -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 +13 -3
- package/runtime/dist/index.d.ts +38 -3
- 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 +30 -13
- package/runtime/src/index.ts +192 -29
- package/src/index.ts +9 -4
package/runtime/src/index.ts
CHANGED
|
@@ -42,41 +42,99 @@ interface Effect {
|
|
|
42
42
|
_dispose(): void;
|
|
43
43
|
}
|
|
44
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
|
+
|
|
45
76
|
export interface EffectStore {
|
|
46
|
-
|
|
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;
|
|
47
84
|
subscribe(onStoreChange: () => void): () => void;
|
|
48
85
|
getSnapshot(): number;
|
|
86
|
+
/** startEffect - begin tracking signals used in this component */
|
|
87
|
+
_start(): void;
|
|
49
88
|
/** finishEffect - stop tracking the signals used in this component */
|
|
50
89
|
f(): void;
|
|
51
90
|
[symDispose](): void;
|
|
52
91
|
}
|
|
53
92
|
|
|
54
|
-
let
|
|
93
|
+
let currentStore: EffectStore | undefined;
|
|
55
94
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
function startComponentEffect(
|
|
96
|
+
prevStore: EffectStore | undefined,
|
|
97
|
+
nextStore: EffectStore
|
|
98
|
+
) {
|
|
99
|
+
const endEffect = nextStore.effect._start();
|
|
100
|
+
currentStore = nextStore;
|
|
101
|
+
|
|
102
|
+
return finishComponentEffect.bind(nextStore, prevStore, endEffect);
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
|
|
105
|
+
function finishComponentEffect(
|
|
106
|
+
this: EffectStore,
|
|
107
|
+
prevStore: EffectStore | undefined,
|
|
108
|
+
endEffect: () => void
|
|
109
|
+
) {
|
|
110
|
+
endEffect();
|
|
111
|
+
currentStore = prevStore;
|
|
112
|
+
}
|
|
64
113
|
|
|
65
114
|
/**
|
|
66
|
-
* 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').
|
|
67
117
|
*
|
|
68
118
|
* React subscribes to this store and gets a snapshot of the current 'version',
|
|
69
|
-
* 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').
|
|
70
121
|
*
|
|
71
|
-
* How we achieve this is by creating a binding with an 'effect', when the
|
|
72
|
-
* 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).
|
|
73
125
|
*
|
|
74
126
|
* [1]
|
|
75
127
|
* @see https://react.dev/reference/react/useSyncExternalStore
|
|
76
|
-
* @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.
|
|
77
134
|
*/
|
|
78
|
-
function createEffectStore(): EffectStore {
|
|
135
|
+
function createEffectStore(_usage: EffectStoreUsage): EffectStore {
|
|
79
136
|
let effectInstance!: Effect;
|
|
137
|
+
let endEffect: (() => void) | undefined;
|
|
80
138
|
let version = 0;
|
|
81
139
|
let onChangeNotifyReact: (() => void) | undefined;
|
|
82
140
|
|
|
@@ -89,6 +147,7 @@ function createEffectStore(): EffectStore {
|
|
|
89
147
|
};
|
|
90
148
|
|
|
91
149
|
return {
|
|
150
|
+
_usage,
|
|
92
151
|
effect: effectInstance,
|
|
93
152
|
subscribe(onStoreChange) {
|
|
94
153
|
onChangeNotifyReact = onStoreChange;
|
|
@@ -112,17 +171,115 @@ function createEffectStore(): EffectStore {
|
|
|
112
171
|
getSnapshot() {
|
|
113
172
|
return version;
|
|
114
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
|
+
},
|
|
115
270
|
f() {
|
|
116
|
-
|
|
271
|
+
endEffect?.();
|
|
272
|
+
endEffect = undefined;
|
|
117
273
|
},
|
|
118
274
|
[symDispose]() {
|
|
119
|
-
|
|
275
|
+
this.f();
|
|
120
276
|
},
|
|
121
277
|
};
|
|
122
278
|
}
|
|
123
279
|
|
|
124
280
|
function createEmptyEffectStore(): EffectStore {
|
|
125
281
|
return {
|
|
282
|
+
_usage: UNMANAGED,
|
|
126
283
|
effect: {
|
|
127
284
|
_sources: undefined,
|
|
128
285
|
_callback() {},
|
|
@@ -137,6 +294,7 @@ function createEmptyEffectStore(): EffectStore {
|
|
|
137
294
|
getSnapshot() {
|
|
138
295
|
return 0;
|
|
139
296
|
},
|
|
297
|
+
_start() {},
|
|
140
298
|
f() {},
|
|
141
299
|
[symDispose]() {},
|
|
142
300
|
};
|
|
@@ -144,30 +302,35 @@ function createEmptyEffectStore(): EffectStore {
|
|
|
144
302
|
|
|
145
303
|
const emptyEffectStore = createEmptyEffectStore();
|
|
146
304
|
|
|
147
|
-
let finalCleanup: Promise<void> | undefined;
|
|
148
305
|
const _queueMicroTask = Promise.prototype.then.bind(Promise.resolve());
|
|
149
306
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* subscribe to changes to rerender the component when the signals change.
|
|
153
|
-
*/
|
|
154
|
-
export function _useSignalsImplementation(): EffectStore {
|
|
155
|
-
clearCurrentStore();
|
|
307
|
+
let finalCleanup: Promise<void> | undefined;
|
|
308
|
+
export function ensureFinalCleanup() {
|
|
156
309
|
if (!finalCleanup) {
|
|
157
310
|
finalCleanup = _queueMicroTask(() => {
|
|
158
311
|
finalCleanup = undefined;
|
|
159
|
-
|
|
312
|
+
currentStore?.f();
|
|
160
313
|
});
|
|
161
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();
|
|
162
325
|
|
|
163
326
|
const storeRef = useRef<EffectStore>();
|
|
164
327
|
if (storeRef.current == null) {
|
|
165
|
-
storeRef.current = createEffectStore();
|
|
328
|
+
storeRef.current = createEffectStore(_usage);
|
|
166
329
|
}
|
|
167
330
|
|
|
168
331
|
const store = storeRef.current;
|
|
169
332
|
useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
170
|
-
|
|
333
|
+
store._start();
|
|
171
334
|
|
|
172
335
|
return store;
|
|
173
336
|
}
|
|
@@ -176,7 +339,7 @@ export function _useSignalsImplementation(): EffectStore {
|
|
|
176
339
|
* A wrapper component that renders a Signal's value directly as a Text node or JSX.
|
|
177
340
|
*/
|
|
178
341
|
function SignalValue({ data }: { data: Signal }) {
|
|
179
|
-
const store =
|
|
342
|
+
const store = _useSignalsImplementation(1);
|
|
180
343
|
try {
|
|
181
344
|
return data.value;
|
|
182
345
|
} finally {
|
|
@@ -197,9 +360,9 @@ Object.defineProperties(Signal.prototype, {
|
|
|
197
360
|
ref: { configurable: true, value: null },
|
|
198
361
|
});
|
|
199
362
|
|
|
200
|
-
export function useSignals(): EffectStore {
|
|
363
|
+
export function useSignals(usage?: EffectStoreUsage): EffectStore {
|
|
201
364
|
if (isAutoSignalTrackingInstalled) return emptyEffectStore;
|
|
202
|
-
return _useSignalsImplementation();
|
|
365
|
+
return _useSignalsImplementation(usage);
|
|
203
366
|
}
|
|
204
367
|
|
|
205
368
|
export function useSignal<T>(value: T): Signal<T> {
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// !!!!!!!!!!!!!!!!!!!!
|
|
2
|
+
//
|
|
3
|
+
// Imports to other packages (e.g. `react` or `@preact/signals-core`) or
|
|
4
|
+
// subpackages (e.g. `@preact/signals-react/runtime`) in this file should be
|
|
5
|
+
// listed as "external" in cmdline arguments passed to microbundle in the root
|
|
6
|
+
// package.json script for this package so their contents aren't bundled into
|
|
7
|
+
// the final source file.
|
|
8
|
+
|
|
1
9
|
import {
|
|
2
10
|
signal,
|
|
3
11
|
computed,
|
|
@@ -12,8 +20,7 @@ import {
|
|
|
12
20
|
useSignal,
|
|
13
21
|
useComputed,
|
|
14
22
|
useSignalEffect,
|
|
15
|
-
|
|
16
|
-
} from "../runtime/src/index"; // TODO: This duplicates runtime code between main and sub runtime packages
|
|
23
|
+
} from "@preact/signals-react/runtime";
|
|
17
24
|
|
|
18
25
|
export {
|
|
19
26
|
signal,
|
|
@@ -33,5 +40,3 @@ declare module "@preact/signals-core" {
|
|
|
33
40
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
34
41
|
interface Signal extends ReactElement {}
|
|
35
42
|
}
|
|
36
|
-
|
|
37
|
-
installAutoSignalTracking();
|