@preact/signals-react 2.3.0 → 3.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.
@@ -1,390 +0,0 @@
1
- import {
2
- // @ts-ignore-next-line
3
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
- __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as ReactInternals,
5
- version as reactVersion,
6
- } from "react";
7
- import React from "react";
8
- import jsxRuntime from "react/jsx-runtime";
9
- import jsxRuntimeDev from "react/jsx-dev-runtime";
10
- import { EffectStore, wrapJsx, _useSignalsImplementation } from "./index";
11
-
12
- export interface ReactDispatcher {
13
- useRef: typeof React.useRef;
14
- useCallback: typeof React.useCallback;
15
- useReducer: typeof React.useReducer;
16
- useSyncExternalStore: typeof React.useSyncExternalStore;
17
- useEffect: typeof React.useEffect;
18
- useImperativeHandle: typeof React.useImperativeHandle;
19
- }
20
-
21
- // In order for signals to work in React, we need to observe what signals a
22
- // component uses while rendering. To do this, we need to know when a component
23
- // is rendering. To do this, we watch the transition of the
24
- // ReactCurrentDispatcher to know when a component is rerendering.
25
- //
26
- // To track when we are entering and exiting a component render (i.e. before and
27
- // after React renders a component), we track how the dispatcher changes.
28
- // Outside of a component rendering, the dispatcher is set to an instance that
29
- // errors or warns when any hooks are called. This behavior is prevents hooks
30
- // from being used outside of components. Right before React renders a
31
- // component, the dispatcher is set to an instance that doesn't warn or error
32
- // and contains the implementations of all hooks. Right after React finishes
33
- // rendering a component, the dispatcher is set to the erroring one again. This
34
- // erroring dispatcher is called the `ContextOnlyDispatcher` in React's source.
35
- //
36
- // So, we watch the getter and setter on `ReactCurrentDispatcher.current` to
37
- // monitor the changes to the current ReactDispatcher. When the dispatcher
38
- // changes from the ContextOnlyDispatcher to a "valid" dispatcher, we assume we
39
- // are entering a component render. At this point, we setup our
40
- // auto-subscriptions for any signals used in the component. We do this by
41
- // creating an Signal effect and manually starting the Signal effect. We use
42
- // `useSyncExternalStore` to trigger rerenders on the component when any signals
43
- // it uses changes.
44
- //
45
- // When the dispatcher changes from a valid dispatcher back to the
46
- // ContextOnlyDispatcher, we assume we are exiting a component render. At this
47
- // point we stop the effect.
48
- //
49
- // Some additional complexities to be aware of:
50
- // - If a component calls `setState` while rendering, React will re-render the
51
- // component immediately. Before triggering the re-render, React will change
52
- // the dispatcher to the HooksDispatcherOnRerender. When we transition to this
53
- // rerendering adapter, we need to re-trigger our hooks to keep the order of
54
- // hooks the same for every render of a component.
55
- //
56
- // - In development, useReducer, useState, and useMemo change the dispatcher to
57
- // a different warning dispatcher (not ContextOnlyDispatcher) before invoking
58
- // the reducer and resets it right after.
59
- //
60
- // The useSyncExternalStore shim will use some of these hooks when we invoke
61
- // it while entering a component render. We need to prevent this dispatcher
62
- // change caused by these hooks from re-triggering our entering logic (it
63
- // would cause an infinite loop if we did not). We do this by using a lock to
64
- // prevent the setter from running while we are in the setter.
65
- //
66
- // When a Component's function body invokes useReducer, useState, or useMemo,
67
- // this change in dispatcher should not signal that we are entering or exiting
68
- // a component render. We ignore this change by detecting these dispatchers as
69
- // different from ContextOnlyDispatcher and other valid dispatchers.
70
- //
71
- // - The `use` hook will change the dispatcher to from a valid update dispatcher
72
- // to a valid mount dispatcher in some cases. Similarly to useReducer
73
- // mentioned above, we should not signal that we are exiting a component
74
- // during this change. Because these other valid dispatchers do not pass the
75
- // ContextOnlyDispatcher check, they do not affect our logic.
76
- //
77
- // - When server rendering, React does not change the dispatcher before and
78
- // after each component render. It sets it once for before the first render
79
- // and once for after the last render. This means that we will not be able to
80
- // detect when we are entering or exiting a component render. This is fine
81
- // because we don't need to detect this for server rendering. A component
82
- // can't trigger async rerenders in SSR so we don't need to track signals.
83
- //
84
- // If a component updates a signal value while rendering during SSR, we will
85
- // not rerender the component because the signal value will synchronously
86
- // change so all reads of the signal further down the tree will see the new
87
- // value.
88
-
89
- /*
90
- Below is a state machine definition for transitions between the various
91
- dispatchers in React's prod build. (It does not include dev time warning
92
- dispatchers which are just always ignored).
93
-
94
- ENTER and EXIT suffixes indicates whether this ReactCurrentDispatcher transition
95
- signals we are entering or exiting a component render, or if it doesn't signal a
96
- change in the component rendering lifecyle (NOOP).
97
-
98
- ```js
99
- // Paste this into https://stately.ai/viz to visualize the state machine.
100
- import { createMachine } from "xstate";
101
-
102
- // ENTER, EXIT, NOOP suffixes indicates whether this ReactCurrentDispatcher
103
- // transition signals we are entering or exiting a component render, or
104
- // if it doesn't signal a change in the component rendering lifecyle (NOOP).
105
-
106
- const dispatcherMachinePROD = createMachine({
107
- id: "ReactCurrentDispatcher_PROD",
108
- initial: "null",
109
- states: {
110
- null: {
111
- on: {
112
- pushDispatcher: "ContextOnlyDispatcher",
113
- },
114
- },
115
- ContextOnlyDispatcher: {
116
- on: {
117
- renderWithHooks_Mount_ENTER: "HooksDispatcherOnMount",
118
- renderWithHooks_Update_ENTER: "HooksDispatcherOnUpdate",
119
- pushDispatcher_NOOP: "ContextOnlyDispatcher",
120
- popDispatcher_NOOP: "ContextOnlyDispatcher",
121
- },
122
- },
123
- HooksDispatcherOnMount: {
124
- on: {
125
- renderWithHooksAgain_ENTER: "HooksDispatcherOnRerender",
126
- resetHooksAfterThrow_EXIT: "ContextOnlyDispatcher",
127
- finishRenderingHooks_EXIT: "ContextOnlyDispatcher",
128
- },
129
- },
130
- HooksDispatcherOnUpdate: {
131
- on: {
132
- renderWithHooksAgain_ENTER: "HooksDispatcherOnRerender",
133
- resetHooksAfterThrow_EXIT: "ContextOnlyDispatcher",
134
- finishRenderingHooks_EXIT: "ContextOnlyDispatcher",
135
- use_ResumeSuspensedMount_NOOP: "HooksDispatcherOnMount",
136
- },
137
- },
138
- HooksDispatcherOnRerender: {
139
- on: {
140
- renderWithHooksAgain_ENTER: "HooksDispatcherOnRerender",
141
- resetHooksAfterThrow_EXIT: "ContextOnlyDispatcher",
142
- finishRenderingHooks_EXIT: "ContextOnlyDispatcher",
143
- },
144
- },
145
- },
146
- });
147
- ```
148
- */
149
-
150
- export let isAutoSignalTrackingInstalled = false;
151
-
152
- let store: EffectStore | null = null;
153
- let lock = false;
154
- let currentDispatcher: ReactDispatcher | null = null;
155
-
156
- function installCurrentDispatcherHook() {
157
- isAutoSignalTrackingInstalled = true;
158
-
159
- Object.defineProperty(ReactInternals.ReactCurrentDispatcher, "current", {
160
- get() {
161
- return currentDispatcher;
162
- },
163
- set(nextDispatcher: ReactDispatcher) {
164
- if (lock) {
165
- currentDispatcher = nextDispatcher;
166
- return;
167
- }
168
-
169
- const currentDispatcherType = getDispatcherType(currentDispatcher);
170
- const nextDispatcherType = getDispatcherType(nextDispatcher);
171
-
172
- // Update the current dispatcher now so the hooks inside of the
173
- // useSyncExternalStore shim get the right dispatcher.
174
- currentDispatcher = nextDispatcher;
175
- if (
176
- isEnteringComponentRender(currentDispatcherType, nextDispatcherType)
177
- ) {
178
- lock = true;
179
- store = _useSignalsImplementation(1);
180
- lock = false;
181
- } else if (
182
- isRestartingComponentRender(currentDispatcherType, nextDispatcherType)
183
- ) {
184
- store?.f();
185
- lock = true;
186
- store = _useSignalsImplementation(1);
187
- lock = false;
188
- } else if (
189
- isExitingComponentRender(currentDispatcherType, nextDispatcherType)
190
- ) {
191
- store?.f();
192
- store = null;
193
- }
194
- },
195
- });
196
- }
197
-
198
- type DispatcherType = number;
199
- const ContextOnlyDispatcherType = 1 << 0;
200
- const WarningDispatcherType = 1 << 1;
201
- const MountDispatcherType = 1 << 2;
202
- const UpdateDispatcherType = 1 << 3;
203
- const RerenderDispatcherType = 1 << 4;
204
- const ServerDispatcherType = 1 << 5;
205
- const BrowserClientDispatcherType =
206
- MountDispatcherType | UpdateDispatcherType | RerenderDispatcherType;
207
-
208
- const dispatcherTypeCache = new Map<ReactDispatcher, DispatcherType>();
209
- function getDispatcherType(dispatcher: ReactDispatcher | null): DispatcherType {
210
- // Treat null the same as the ContextOnlyDispatcher.
211
- if (!dispatcher) return ContextOnlyDispatcherType;
212
-
213
- const cached = dispatcherTypeCache.get(dispatcher);
214
- if (cached !== undefined) return cached;
215
-
216
- // The ContextOnlyDispatcher sets all the hook implementations to a function
217
- // that takes no arguments and throws and error. This dispatcher is the only
218
- // dispatcher where useReducer and useEffect will have the same
219
- // implementation.
220
- let type: DispatcherType;
221
- const useCallbackImpl = dispatcher.useCallback.toString();
222
- if (dispatcher.useReducer === dispatcher.useEffect) {
223
- type = ContextOnlyDispatcherType;
224
-
225
- // @ts-expect-error When server rendering, useEffect and useImperativeHandle
226
- // are both set to noop functions and so have the same implementation.
227
- } else if (dispatcher.useEffect === dispatcher.useImperativeHandle) {
228
- type = ServerDispatcherType;
229
- } else if (/Invalid/.test(useCallbackImpl)) {
230
- // We first check for warning dispatchers because they would also pass some
231
- // of the checks below.
232
- type = WarningDispatcherType;
233
- } else if (
234
- // The development mount dispatcher invokes a function called
235
- // `mountCallback` whereas the development update/re-render dispatcher
236
- // invokes a function called `updateCallback`. Use that difference to
237
- // determine if we are in a mount or update-like dispatcher in development.
238
- // The production mount dispatcher defines an array of the form [callback,
239
- // deps] whereas update/re-render dispatchers read the array using array
240
- // indices (e.g. `[0]` and `[1]`). Use those differences to determine if we
241
- // are in a mount or update-like dispatcher in production.
242
- /updateCallback/.test(useCallbackImpl) ||
243
- (/\[0\]/.test(useCallbackImpl) && /\[1\]/.test(useCallbackImpl))
244
- ) {
245
- // The update and rerender dispatchers have different implementations for
246
- // useReducer. We'll check it's implementation to determine if this is the
247
- // rerender or update dispatcher.
248
- let useReducerImpl = dispatcher.useReducer.toString();
249
- if (
250
- // The development rerender dispatcher invokes a function called
251
- // `rerenderReducer` whereas the update dispatcher invokes a function
252
- // called `updateReducer`. The production rerender dispatcher returns an
253
- // array of the form `[state, dispatch]` whereas the update dispatcher
254
- // returns an array of `[fiber.memoizedState, dispatch]` so we check the
255
- // return statement in the implementation of useReducer to differentiate
256
- // between the two.
257
- /rerenderReducer/.test(useReducerImpl) ||
258
- /return\s*\[\w+,/.test(useReducerImpl)
259
- ) {
260
- type = RerenderDispatcherType;
261
- } else {
262
- type = UpdateDispatcherType;
263
- }
264
- } else {
265
- type = MountDispatcherType;
266
- }
267
-
268
- dispatcherTypeCache.set(dispatcher, type);
269
- return type;
270
- }
271
-
272
- function isEnteringComponentRender(
273
- currentDispatcherType: DispatcherType,
274
- nextDispatcherType: DispatcherType
275
- ): boolean {
276
- if (
277
- currentDispatcherType & ContextOnlyDispatcherType &&
278
- nextDispatcherType & BrowserClientDispatcherType
279
- ) {
280
- // ## Mount or update (ContextOnlyDispatcher -> ValidDispatcher (Mount or Update))
281
- //
282
- // If the current dispatcher is the ContextOnlyDispatcher and the next
283
- // dispatcher is a valid dispatcher, we are entering a component render.
284
- return true;
285
- } else if (
286
- currentDispatcherType & WarningDispatcherType ||
287
- nextDispatcherType & WarningDispatcherType
288
- ) {
289
- // ## Warning dispatcher
290
- //
291
- // If the current dispatcher or next dispatcher is an warning dispatcher,
292
- // we are not entering a component render. The current warning dispatchers
293
- // are used to warn when hooks are nested improperly and do not indicate
294
- // entering a new component render.
295
- return false;
296
- } else {
297
- // ## Resuming suspended mount edge case (Update -> Mount)
298
- //
299
- // If we are transitioning from the update dispatcher to the mount
300
- // dispatcher, then this component is using the `use` hook and is resuming
301
- // from a mount. We should not re-invoke our hooks in this situation since
302
- // we are not entering a new component render, but instead continuing a
303
- // previous render.
304
- //
305
- // ## Other transitions
306
- //
307
- // For example, Mount -> Mount, Update -> Update, Mount -> Update, any
308
- // transition in and out of invalid dispatchers.
309
- //
310
- // There is no known transition for the following transitions so we default
311
- // to not triggering a re-enter of the component.
312
- // - HooksDispatcherOnMount -> HooksDispatcherOnMount
313
- // - HooksDispatcherOnMount -> HooksDispatcherOnUpdate
314
- // - HooksDispatcherOnUpdate -> HooksDispatcherOnUpdate
315
- return false;
316
- }
317
- }
318
-
319
- function isRestartingComponentRender(
320
- currentDispatcherType: DispatcherType,
321
- nextDispatcherType: DispatcherType
322
- ): boolean {
323
- // A transition from a valid browser dispatcher into the rerender dispatcher
324
- // is the restart of a component render, so we should end the current
325
- // component effect and re-invoke our hooks. Details below.
326
- //
327
- // ## In-place rerendering (e.g. Mount -> Rerender)
328
- //
329
- // If we are transitioning from the mount, update, or rerender dispatcher to
330
- // the rerender dispatcher (e.g. HooksDispatcherOnMount to
331
- // HooksDispatcherOnRerender), then this component is rerendering due to
332
- // calling setState inside of its function body. We are re-entering a
333
- // component's render method and so we should re-invoke our hooks.
334
-
335
- return Boolean(
336
- currentDispatcherType & BrowserClientDispatcherType &&
337
- nextDispatcherType & RerenderDispatcherType
338
- );
339
- }
340
-
341
- /**
342
- * We are exiting a component render if the current dispatcher is a valid
343
- * dispatcher and the next dispatcher is the ContextOnlyDispatcher.
344
- */
345
- function isExitingComponentRender(
346
- currentDispatcherType: DispatcherType,
347
- nextDispatcherType: DispatcherType
348
- ): boolean {
349
- return Boolean(
350
- currentDispatcherType & BrowserClientDispatcherType &&
351
- nextDispatcherType & ContextOnlyDispatcherType
352
- );
353
- }
354
-
355
- interface JsxRuntimeModule {
356
- jsx?(type: any, ...rest: any[]): unknown;
357
- jsxs?(type: any, ...rest: any[]): unknown;
358
- jsxDEV?(type: any, ...rest: any[]): unknown;
359
- }
360
-
361
- export function installJSXHooks() {
362
- const JsxPro: JsxRuntimeModule = jsxRuntime;
363
- const JsxDev: JsxRuntimeModule = jsxRuntimeDev;
364
-
365
- /**
366
- * createElement _may_ be called by jsx runtime as a fallback in certain cases,
367
- * so we need to wrap it regardless.
368
- *
369
- * The jsx exports depend on the `NODE_ENV` var to ensure the users' bundler doesn't
370
- * include both, so one of them will be set with `undefined` values.
371
- */
372
- React.createElement = wrapJsx(React.createElement);
373
- JsxDev.jsx && /* */ (JsxDev.jsx = wrapJsx(JsxDev.jsx));
374
- JsxPro.jsx && /* */ (JsxPro.jsx = wrapJsx(JsxPro.jsx));
375
- JsxDev.jsxs && /* */ (JsxDev.jsxs = wrapJsx(JsxDev.jsxs));
376
- JsxPro.jsxs && /* */ (JsxPro.jsxs = wrapJsx(JsxPro.jsxs));
377
- JsxDev.jsxDEV && /**/ (JsxDev.jsxDEV = wrapJsx(JsxDev.jsxDEV));
378
- JsxPro.jsxDEV && /**/ (JsxPro.jsxDEV = wrapJsx(JsxPro.jsxDEV));
379
- }
380
-
381
- export function installAutoSignalTracking() {
382
- const [major] = reactVersion.split(".").map(Number);
383
- if (major >= 19) {
384
- throw new Error(
385
- "Automatic signals tracking is not supported in React 19 and later, try the Babel plugin instead https://github.com/preactjs/signals/tree/main/packages/react-transform#signals-react-transform."
386
- );
387
- }
388
- installCurrentDispatcherHook();
389
- installJSXHooks();
390
- }