Wire up the native API for useSyncExternalStore (#22237)
Adds useSyncExternalStore to the internal dispatcher, and exports the native API from the React package without yet implementing it.
Andrew Clark committed
Sep 7, 2021 at 13:20 UTC
77912d9a05d7a90287fabdec76486f25869b2981
17 files changed
+311
-61
packages/react-debug-tools/src/ReactDebugHooks.js
+8
@@ -265,6 +265,13 @@ function useMutableSource<Source, Snapshot>(
265
return value;
266
}
267
268
+function useSyncExternalStore<T>(
269
+ subscribe: (() => void) => () => void,
270
+ getSnapshot: () => T,
271
+): T {
272
+ throw new Error('Not yet implemented');
273
+}
274
+
275
function useTransition(): [boolean, (() => void) => void] {
276
// useTransition() composes multiple hooks internally.
277
// Advance the current hook index the same number of times
@@ -326,6 +333,7 @@ const Dispatcher: DispatcherType = {
333
useState,
334
useTransition,
335
useMutableSource,
336
+ useSyncExternalStore,
337
useDeferredValue,
338
useOpaqueIdentifier,
339
};
packages/react-dom/src/server/ReactPartialRendererHooks.js
+8
@@ -462,6 +462,13 @@ function useMutableSource<Source, Snapshot>(
462
return getSnapshot(source._source);
463
}
464
465
+function useSyncExternalStore<T>(
466
+ subscribe: (() => void) => () => void,
467
+ getSnapshot: () => T,
468
+): T {
469
+ throw new Error('Not yet implemented');
470
+}
471
+
472
function useDeferredValue<T>(value: T): T {
473
resolveCurrentlyRenderingComponent();
474
return value;
@@ -514,6 +521,7 @@ export const Dispatcher: DispatcherType = {
521
useOpaqueIdentifier,
522
// Subscriptions are not setup in a server environment.
523
useMutableSource,
524
+ useSyncExternalStore,
525
};
526
527
if (enableCache) {
packages/react-reconciler/src/ReactFiberHooks.new.js
+77
@@ -1242,6 +1242,20 @@ function updateMutableSource<Source, Snapshot>(
1242
return useMutableSource(hook, source, getSnapshot, subscribe);
1243
}
1244
1245
+function mountSyncExternalStore<T>(
1246
+ subscribe: (() => void) => () => void,
1247
+ getSnapshot: () => T,
1248
+): T {
1249
+ throw new Error('Not yet implemented');
1250
+}
1251
+
1252
+function updateSyncExternalStore<T>(
1253
+ subscribe: (() => void) => () => void,
1254
+ getSnapshot: () => T,
1255
+): T {
1256
+ throw new Error('Not yet implemented');
1257
+}
1258
+
1259
function mountState<S>(
1260
initialState: (() => S) | S,
1261
): [S, Dispatch<BasicStateAction<S>>] {
@@ -2079,6 +2093,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2093
useDeferredValue: throwInvalidHookError,
2094
useTransition: throwInvalidHookError,
2095
useMutableSource: throwInvalidHookError,
2096
+ useSyncExternalStore: throwInvalidHookError,
2097
useOpaqueIdentifier: throwInvalidHookError,
2098
2099
unstable_isNewReconciler: enableNewReconciler,
@@ -2104,6 +2119,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2119
useDeferredValue: mountDeferredValue,
2120
useTransition: mountTransition,
2121
useMutableSource: mountMutableSource,
2122
+ useSyncExternalStore: mountSyncExternalStore,
2123
useOpaqueIdentifier: mountOpaqueIdentifier,
2124
2125
unstable_isNewReconciler: enableNewReconciler,
@@ -2129,6 +2145,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2145
useDeferredValue: updateDeferredValue,
2146
useTransition: updateTransition,
2147
useMutableSource: updateMutableSource,
2148
+ useSyncExternalStore: updateSyncExternalStore,
2149
useOpaqueIdentifier: updateOpaqueIdentifier,
2150
2151
unstable_isNewReconciler: enableNewReconciler,
@@ -2154,6 +2171,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2171
useDeferredValue: rerenderDeferredValue,
2172
useTransition: rerenderTransition,
2173
useMutableSource: updateMutableSource,
2174
+ useSyncExternalStore: mountSyncExternalStore,
2175
useOpaqueIdentifier: rerenderOpaqueIdentifier,
2176
2177
unstable_isNewReconciler: enableNewReconciler,
@@ -2302,6 +2320,14 @@ if (__DEV__) {
2320
mountHookTypesDev();
2321
return mountMutableSource(source, getSnapshot, subscribe);
2322
},
2323
+ useSyncExternalStore<T>(
2324
+ subscribe: (() => void) => () => void,
2325
+ getSnapshot: () => T,
2326
+ ): T {
2327
+ currentHookNameInDev = 'useSyncExternalStore';
2328
+ mountHookTypesDev();
2329
+ return mountSyncExternalStore(subscribe, getSnapshot);
2330
+ },
2331
useOpaqueIdentifier(): OpaqueIDType | void {
2332
currentHookNameInDev = 'useOpaqueIdentifier';
2333
mountHookTypesDev();
@@ -2426,6 +2452,14 @@ if (__DEV__) {
2452
updateHookTypesDev();
2453
return mountMutableSource(source, getSnapshot, subscribe);
2454
},
2455
+ useSyncExternalStore<T>(
2456
+ subscribe: (() => void) => () => void,
2457
+ getSnapshot: () => T,
2458
+ ): T {
2459
+ currentHookNameInDev = 'useSyncExternalStore';
2460
+ updateHookTypesDev();
2461
+ return mountSyncExternalStore(subscribe, getSnapshot);
2462
+ },
2463
useOpaqueIdentifier(): OpaqueIDType | void {
2464
currentHookNameInDev = 'useOpaqueIdentifier';
2465
updateHookTypesDev();
@@ -2550,6 +2584,14 @@ if (__DEV__) {
2584
updateHookTypesDev();
2585
return updateMutableSource(source, getSnapshot, subscribe);
2586
},
2587
+ useSyncExternalStore<T>(
2588
+ subscribe: (() => void) => () => void,
2589
+ getSnapshot: () => T,
2590
+ ): T {
2591
+ currentHookNameInDev = 'useSyncExternalStore';
2592
+ updateHookTypesDev();
2593
+ return updateSyncExternalStore(subscribe, getSnapshot);
2594
+ },
2595
useOpaqueIdentifier(): OpaqueIDType | void {
2596
currentHookNameInDev = 'useOpaqueIdentifier';
2597
updateHookTypesDev();
@@ -2675,6 +2717,14 @@ if (__DEV__) {
2717
updateHookTypesDev();
2718
return updateMutableSource(source, getSnapshot, subscribe);
2719
},
2720
+ useSyncExternalStore<T>(
2721
+ subscribe: (() => void) => () => void,
2722
+ getSnapshot: () => T,
2723
+ ): T {
2724
+ currentHookNameInDev = 'useSyncExternalStore';
2725
+ updateHookTypesDev();
2726
+ return updateSyncExternalStore(subscribe, getSnapshot);
2727
+ },
2728
useOpaqueIdentifier(): OpaqueIDType | void {
2729
currentHookNameInDev = 'useOpaqueIdentifier';
2730
updateHookTypesDev();
@@ -2813,6 +2863,15 @@ if (__DEV__) {
2863
mountHookTypesDev();
2864
return mountMutableSource(source, getSnapshot, subscribe);
2865
},
2866
+ useSyncExternalStore<T>(
2867
+ subscribe: (() => void) => () => void,
2868
+ getSnapshot: () => T,
2869
+ ): T {
2870
+ currentHookNameInDev = 'useSyncExternalStore';
2871
+ warnInvalidHookAccess();
2872
+ mountHookTypesDev();
2873
+ return mountSyncExternalStore(subscribe, getSnapshot);
2874
+ },
2875
useOpaqueIdentifier(): OpaqueIDType | void {
2876
currentHookNameInDev = 'useOpaqueIdentifier';
2877
warnInvalidHookAccess();
@@ -2952,6 +3011,15 @@ if (__DEV__) {
3011
updateHookTypesDev();
3012
return updateMutableSource(source, getSnapshot, subscribe);
3013
},
3014
+ useSyncExternalStore<T>(
3015
+ subscribe: (() => void) => () => void,
3016
+ getSnapshot: () => T,
3017
+ ): T {
3018
+ currentHookNameInDev = 'useSyncExternalStore';
3019
+ warnInvalidHookAccess();
3020
+ updateHookTypesDev();
3021
+ return updateSyncExternalStore(subscribe, getSnapshot);
3022
+ },
3023
useOpaqueIdentifier(): OpaqueIDType | void {
3024
currentHookNameInDev = 'useOpaqueIdentifier';
3025
warnInvalidHookAccess();
@@ -3092,6 +3160,15 @@ if (__DEV__) {
3160
updateHookTypesDev();
3161
return updateMutableSource(source, getSnapshot, subscribe);
3162
},
3163
+ useSyncExternalStore<T>(
3164
+ subscribe: (() => void) => () => void,
3165
+ getSnapshot: () => T,
3166
+ ): T {
3167
+ currentHookNameInDev = 'useSyncExternalStore';
3168
+ warnInvalidHookAccess();
3169
+ updateHookTypesDev();
3170
+ return updateSyncExternalStore(subscribe, getSnapshot);
3171
+ },
3172
useOpaqueIdentifier(): OpaqueIDType | void {
3173
currentHookNameInDev = 'useOpaqueIdentifier';
3174
warnInvalidHookAccess();
packages/react-reconciler/src/ReactFiberHooks.old.js
+77
@@ -1242,6 +1242,20 @@ function updateMutableSource<Source, Snapshot>(
1242
return useMutableSource(hook, source, getSnapshot, subscribe);
1243
}
1244
1245
+function mountSyncExternalStore<T>(
1246
+ subscribe: (() => void) => () => void,
1247
+ getSnapshot: () => T,
1248
+): T {
1249
+ throw new Error('Not yet implemented');
1250
+}
1251
+
1252
+function updateSyncExternalStore<T>(
1253
+ subscribe: (() => void) => () => void,
1254
+ getSnapshot: () => T,
1255
+): T {
1256
+ throw new Error('Not yet implemented');
1257
+}
1258
+
1259
function mountState<S>(
1260
initialState: (() => S) | S,
1261
): [S, Dispatch<BasicStateAction<S>>] {
@@ -2079,6 +2093,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2093
useDeferredValue: throwInvalidHookError,
2094
useTransition: throwInvalidHookError,
2095
useMutableSource: throwInvalidHookError,
2096
+ useSyncExternalStore: throwInvalidHookError,
2097
useOpaqueIdentifier: throwInvalidHookError,
2098
2099
unstable_isNewReconciler: enableNewReconciler,
@@ -2104,6 +2119,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2119
useDeferredValue: mountDeferredValue,
2120
useTransition: mountTransition,
2121
useMutableSource: mountMutableSource,
2122
+ useSyncExternalStore: mountSyncExternalStore,
2123
useOpaqueIdentifier: mountOpaqueIdentifier,
2124
2125
unstable_isNewReconciler: enableNewReconciler,
@@ -2129,6 +2145,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2145
useDeferredValue: updateDeferredValue,
2146
useTransition: updateTransition,
2147
useMutableSource: updateMutableSource,
2148
+ useSyncExternalStore: updateSyncExternalStore,
2149
useOpaqueIdentifier: updateOpaqueIdentifier,
2150
2151
unstable_isNewReconciler: enableNewReconciler,
@@ -2154,6 +2171,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2171
useDeferredValue: rerenderDeferredValue,
2172
useTransition: rerenderTransition,
2173
useMutableSource: updateMutableSource,
2174
+ useSyncExternalStore: mountSyncExternalStore,
2175
useOpaqueIdentifier: rerenderOpaqueIdentifier,
2176
2177
unstable_isNewReconciler: enableNewReconciler,
@@ -2302,6 +2320,14 @@ if (__DEV__) {
2320
mountHookTypesDev();
2321
return mountMutableSource(source, getSnapshot, subscribe);
2322
},
2323
+ useSyncExternalStore<T>(
2324
+ subscribe: (() => void) => () => void,
2325
+ getSnapshot: () => T,
2326
+ ): T {
2327
+ currentHookNameInDev = 'useSyncExternalStore';
2328
+ mountHookTypesDev();
2329
+ return mountSyncExternalStore(subscribe, getSnapshot);
2330
+ },
2331
useOpaqueIdentifier(): OpaqueIDType | void {
2332
currentHookNameInDev = 'useOpaqueIdentifier';
2333
mountHookTypesDev();
@@ -2426,6 +2452,14 @@ if (__DEV__) {
2452
updateHookTypesDev();
2453
return mountMutableSource(source, getSnapshot, subscribe);
2454
},
2455
+ useSyncExternalStore<T>(
2456
+ subscribe: (() => void) => () => void,
2457
+ getSnapshot: () => T,
2458
+ ): T {
2459
+ currentHookNameInDev = 'useSyncExternalStore';
2460
+ updateHookTypesDev();
2461
+ return mountSyncExternalStore(subscribe, getSnapshot);
2462
+ },
2463
useOpaqueIdentifier(): OpaqueIDType | void {
2464
currentHookNameInDev = 'useOpaqueIdentifier';
2465
updateHookTypesDev();
@@ -2550,6 +2584,14 @@ if (__DEV__) {
2584
updateHookTypesDev();
2585
return updateMutableSource(source, getSnapshot, subscribe);
2586
},
2587
+ useSyncExternalStore<T>(
2588
+ subscribe: (() => void) => () => void,
2589
+ getSnapshot: () => T,
2590
+ ): T {
2591
+ currentHookNameInDev = 'useSyncExternalStore';
2592
+ updateHookTypesDev();
2593
+ return updateSyncExternalStore(subscribe, getSnapshot);
2594
+ },
2595
useOpaqueIdentifier(): OpaqueIDType | void {
2596
currentHookNameInDev = 'useOpaqueIdentifier';
2597
updateHookTypesDev();
@@ -2675,6 +2717,14 @@ if (__DEV__) {
2717
updateHookTypesDev();
2718
return updateMutableSource(source, getSnapshot, subscribe);
2719
},
2720
+ useSyncExternalStore<T>(
2721
+ subscribe: (() => void) => () => void,
2722
+ getSnapshot: () => T,
2723
+ ): T {
2724
+ currentHookNameInDev = 'useSyncExternalStore';
2725
+ updateHookTypesDev();
2726
+ return updateSyncExternalStore(subscribe, getSnapshot);
2727
+ },
2728
useOpaqueIdentifier(): OpaqueIDType | void {
2729
currentHookNameInDev = 'useOpaqueIdentifier';
2730
updateHookTypesDev();
@@ -2813,6 +2863,15 @@ if (__DEV__) {
2863
mountHookTypesDev();
2864
return mountMutableSource(source, getSnapshot, subscribe);
2865
},
2866
+ useSyncExternalStore<T>(
2867
+ subscribe: (() => void) => () => void,
2868
+ getSnapshot: () => T,
2869
+ ): T {
2870
+ currentHookNameInDev = 'useSyncExternalStore';
2871
+ warnInvalidHookAccess();
2872
+ mountHookTypesDev();
2873
+ return mountSyncExternalStore(subscribe, getSnapshot);
2874
+ },
2875
useOpaqueIdentifier(): OpaqueIDType | void {
2876
currentHookNameInDev = 'useOpaqueIdentifier';
2877
warnInvalidHookAccess();
@@ -2952,6 +3011,15 @@ if (__DEV__) {
3011
updateHookTypesDev();
3012
return updateMutableSource(source, getSnapshot, subscribe);
3013
},
3014
+ useSyncExternalStore<T>(
3015
+ subscribe: (() => void) => () => void,
3016
+ getSnapshot: () => T,
3017
+ ): T {
3018
+ currentHookNameInDev = 'useSyncExternalStore';
3019
+ warnInvalidHookAccess();
3020
+ updateHookTypesDev();
3021
+ return updateSyncExternalStore(subscribe, getSnapshot);
3022
+ },
3023
useOpaqueIdentifier(): OpaqueIDType | void {
3024
currentHookNameInDev = 'useOpaqueIdentifier';
3025
warnInvalidHookAccess();
@@ -3092,6 +3160,15 @@ if (__DEV__) {
3160
updateHookTypesDev();
3161
return updateMutableSource(source, getSnapshot, subscribe);
3162
},
3163
+ useSyncExternalStore<T>(
3164
+ subscribe: (() => void) => () => void,
3165
+ getSnapshot: () => T,
3166
+ ): T {
3167
+ currentHookNameInDev = 'useSyncExternalStore';
3168
+ warnInvalidHookAccess();
3169
+ updateHookTypesDev();
3170
+ return updateSyncExternalStore(subscribe, getSnapshot);
3171
+ },
3172
useOpaqueIdentifier(): OpaqueIDType | void {
3173
currentHookNameInDev = 'useOpaqueIdentifier';
3174
warnInvalidHookAccess();
packages/react-reconciler/src/ReactInternalTypes.js
+5
@@ -41,6 +41,7 @@ export type HookType =
41
| 'useDeferredValue'
42
| 'useTransition'
43
| 'useMutableSource'
44
+ | 'useSyncExternalStore'
45
| 'useOpaqueIdentifier'
46
| 'useCacheRefresh';
47
@@ -304,6 +305,10 @@ export type Dispatcher = {|
305
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
306
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
307
): Snapshot,
308
+ useSyncExternalStore<T>(
309
+ subscribe: (() => void) => () => void,
310
+ getSnapshot: () => T,
311
+ ): T,
312
useOpaqueIdentifier(): any,
313
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
314
packages/react-server/src/ReactFizzHooks.js
+8
@@ -461,6 +461,13 @@ function useMutableSource<Source, Snapshot>(
461
return getSnapshot(source._source);
462
}
463
464
+function useSyncExternalStore<T>(
465
+ subscribe: (() => void) => () => void,
466
+ getSnapshot: () => T,
467
+): T {
468
+ throw new Error('Not yet implemented');
469
+}
470
+
471
function useDeferredValue<T>(value: T): T {
472
resolveCurrentlyRenderingComponent();
473
return value;
@@ -509,6 +516,7 @@ export const Dispatcher: DispatcherType = {
516
useOpaqueIdentifier,
517
// Subscriptions are not setup in a server environment.
518
useMutableSource,
519
+ useSyncExternalStore,
520
};
521
522
if (enableCache) {
packages/react-server/src/ReactFlightServer.js
+1
@@ -827,6 +827,7 @@ const Dispatcher: DispatcherType = {
827
useEffect: (unsupportedHook: any),
828
useOpaqueIdentifier: (unsupportedHook: any),
829
useMutableSource: (unsupportedHook: any),
830
+ useSyncExternalStore: (unsupportedHook: any),
831
useCacheRefresh(): <T>(?() => T, ?T) => void {
832
return unsupportedRefresh;
833
},
packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+1
@@ -44,6 +44,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
44
useTransition: unsupported,
45
useOpaqueIdentifier: unsupported,
46
useMutableSource: unsupported,
47
+ useSyncExternalStore: unsupported,
48
useCacheRefresh: unsupported,
49
};
50
// Not using async/await because we don't compile it.
packages/react/index.classic.fb.js
+2
@@ -51,6 +51,8 @@ export {
51
useMemo,
52
useMutableSource,
53
useMutableSource as unstable_useMutableSource,
54
+ useSyncExternalStore,
55
+ useSyncExternalStore as unstable_useSyncExternalStore,
56
useReducer,
57
useRef,
58
useState,
packages/react/index.experimental.js
+1
@@ -45,6 +45,7 @@ export {
45
useLayoutEffect,
46
useMemo,
47
useMutableSource as unstable_useMutableSource,
48
+ useSyncExternalStore as unstable_useSyncExternalStore,
49
useReducer,
50
useRef,
51
useState,
packages/react/index.js
+2
@@ -70,6 +70,8 @@ export {
70
useLayoutEffect,
71
useMemo,
72
useMutableSource,
73
+ useSyncExternalStore,
74
+ useSyncExternalStore as unstable_useSyncExternalStore,
75
useReducer,
76
useRef,
77
useState,
packages/react/index.modern.fb.js
+2
@@ -50,6 +50,8 @@ export {
50
useMemo,
51
useMutableSource,
52
useMutableSource as unstable_useMutableSource,
53
+ useSyncExternalStore,
54
+ useSyncExternalStore as unstable_useSyncExternalStore,
55
useReducer,
56
useRef,
57
useState,
packages/react/src/React.js
+2
@@ -44,6 +44,7 @@ import {
44
useLayoutEffect,
45
useMemo,
46
useMutableSource,
47
+ useSyncExternalStore,
48
useReducer,
49
useRef,
50
useState,
@@ -93,6 +94,7 @@ export {
94
useLayoutEffect,
95
useMemo,
96
useMutableSource,
97
+ useSyncExternalStore,
98
useReducer,
99
useRef,
100
useState,
packages/react/src/ReactHooks.js
+8
@@ -169,6 +169,14 @@ export function useMutableSource<Source, Snapshot>(
169
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
170
}
171
172
+export function useSyncExternalStore<T>(
173
+ subscribe: (() => void) => () => void,
174
+ getSnapshot: () => T,
175
+): T {
176
+ const dispatcher = resolveDispatcher();
177
+ return dispatcher.useSyncExternalStore(subscribe, getSnapshot);
178
+}
179
+
180
export function useCacheRefresh(): <T>(?() => T, ?T) => void {
181
const dispatcher = resolveDispatcher();
182
// $FlowFixMe This is unstable, thus optional
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+104
-58
@@ -29,11 +29,28 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
29
// use the shim.
30
// TODO: Don't do this during a variant test run. That way these tests run
31
// against both the shim and the built-in implementation.
32
- jest.mock('react', () => {
33
- // eslint-disable-next-line no-unused-vars
34
- const {startTransition, ...otherExports} = jest.requireActual('react');
35
- return otherExports;
36
- });
32
+ if (gate(flags => flags.variant)) {
33
+ // We'll use the variant flag to represent the native implementation
34
+ } else {
35
+ // and the non-variant tests for the shim.
36
+ //
37
+ // Remove useSyncExternalStore from the React imports so that we use the
38
+ // shim instead. Also removing startTransition, since we use that to
39
+ // detect outdated 18 alphas that don't yet include useSyncExternalStore.
40
+ //
41
+ // Longer term, we'll probably test this branch using an actual build
42
+ // of React 17.
43
+ jest.mock('react', () => {
44
+ const {
45
+ // eslint-disable-next-line no-unused-vars
46
+ startTransition: _,
47
+ // eslint-disable-next-line no-unused-vars
48
+ useSyncExternalStore: __,
49
+ ...otherExports
50
+ } = jest.requireActual('react');
51
+ return otherExports;
52
+ });
53
+ }
54
55
React = require('react');
56
ReactNoop = require('react-noop-renderer');
@@ -63,14 +80,17 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
80
function createRoot(element) {
81
// This wrapper function exists so we can test both legacy roots and
82
// concurrent roots.
66
- //
67
- // TODO: Once the built-in API exists, conditionally test the concurrent
68
- // root API, too.
69
- const root = ReactNoop.createLegacyRoot();
70
- act(() => {
71
- root.render(element);
72
- });
73
- return root;
83
+ if (gate(flags => flags.variant)) {
84
+ // The native implementation only exists in 18+, so we test using
85
+ // concurrent mode. To test the legacy root behavior in the native
86
+ // implementation (which is supported in the sense that it needs to have
87
+ // the correct behavior, despite the fact that the legacy root API
88
+ // triggers a warning in 18), write a test that uses
89
+ // createLegacyRoot directly.
90
+ return ReactNoop.createRoot();
91
+ } else {
92
+ return ReactNoop.createLegacyRoot();
93
+ }
94
}
95
96
function createExternalStore(initialState) {
@@ -96,6 +116,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
116
};
117
}
118
119
+ // @gate !variant
120
test('basic usage', () => {
121
const store = createExternalStore('Initial');
122
@@ -104,7 +125,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
125
return <Text text={text} />;
126
}
127
107
- const root = createRoot(<App />);
128
+ const root = createRoot();
129
+ act(() => root.render(<App />));
130
131
expect(Scheduler).toHaveYielded(['Initial']);
132
expect(root).toMatchRenderedOutput('Initial');
@@ -116,6 +138,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
138
expect(root).toMatchRenderedOutput('Updated');
139
});
140
141
+ // @gate !variant
142
test('skips re-rendering if nothing changes', () => {
143
const store = createExternalStore('Initial');
144
@@ -124,7 +147,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
147
return <Text text={text} />;
148
}
149
127
- const root = createRoot(<App />);
150
+ const root = createRoot();
151
+ act(() => root.render(<App />));
152
153
expect(Scheduler).toHaveYielded(['Initial']);
154
expect(root).toMatchRenderedOutput('Initial');
@@ -138,6 +162,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
162
expect(root).toMatchRenderedOutput('Initial');
163
});
164
165
+ // @gate !variant
166
test('switch to a different store', () => {
167
const storeA = createExternalStore(0);
168
const storeB = createExternalStore(0);
@@ -150,7 +175,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
175
return <Text text={value} />;
176
}
177
153
- const root = createRoot(<App />);
178
+ const root = createRoot();
179
+ act(() => root.render(<App />));
180
181
expect(Scheduler).toHaveYielded([0]);
182
expect(root).toMatchRenderedOutput('0');
@@ -187,6 +213,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
213
expect(root).toMatchRenderedOutput('1');
214
});
215
216
+ // @gate !variant
217
test('selecting a specific value inside getSnapshot', () => {
218
const store = createExternalStore({a: 0, b: 0});
219
@@ -208,7 +235,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
235
);
236
}
237
211
- const root = createRoot(<App />);
238
+ const root = createRoot();
239
+ act(() => root.render(<App />));
240
241
expect(Scheduler).toHaveYielded(['A0', 'B0']);
242
expect(root).toMatchRenderedOutput('A0B0');
@@ -230,6 +258,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
258
expect(root).toMatchRenderedOutput('A1B1');
259
});
260
261
+ // @gate !variant
262
test(
263
"compares to current state before bailing out, even when there's a " +
264
'mutation in between the sync and passive effects',
@@ -244,7 +273,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
273
return <Text text={value} />;
274
}
275
247
- const root = createRoot(<App />);
276
+ const root = createRoot();
277
+ act(() => root.render(<App />));
278
expect(Scheduler).toHaveYielded([0, 'Passive effect: 0']);
279
280
// Schedule an update. We'll intentionally not use `act` so that we can
@@ -272,6 +302,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
302
},
303
);
304
305
+ // @gate !variant
306
test('mutating the store in between render and commit when getSnapshot has changed', () => {
307
const store = createExternalStore({a: 1, b: 1});
308
@@ -312,7 +343,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
343
);
344
}
345
315
- const root = createRoot(<App />);
346
+ const root = createRoot();
347
+ act(() => root.render(<App />));
348
expect(Scheduler).toHaveYielded(['A1']);
349
expect(root).toMatchRenderedOutput('A1');
350
@@ -330,6 +362,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
362
expect(root).toMatchRenderedOutput('B2');
363
});
364
365
+ // @gate !variant
366
test('mutating the store in between render and commit when getSnapshot has _not_ changed', () => {
367
// Same as previous test, but `getSnapshot` does not change
368
const store = createExternalStore({a: 1, b: 1});
@@ -368,7 +401,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
401
);
402
}
403
371
- const root = createRoot(<App />);
404
+ const root = createRoot();
405
+ act(() => root.render(<App />));
406
expect(Scheduler).toHaveYielded(['A1']);
407
expect(root).toMatchRenderedOutput('A1');
408
@@ -387,6 +421,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
421
expect(root).toMatchRenderedOutput('A1');
422
});
423
424
+ // @gate !variant
425
test("does not bail out if the previous update hasn't finished yet", () => {
426
const store = createExternalStore(0);
427
@@ -406,11 +441,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
441
return <Text text={value} />;
442
}
443
409
- const root = createRoot(
410
- <>
411
- <Child1 />
412
- <Child2 />
413
- </>,
444
+ const root = createRoot();
445
+ act(() =>
446
+ root.render(
447
+ <>
448
+ <Child1 />
449
+ <Child2 />
450
+ </>,
451
+ ),
452
);
453
expect(Scheduler).toHaveYielded([0, 0]);
454
expect(root).toMatchRenderedOutput('00');
@@ -422,6 +460,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
460
expect(root).toMatchRenderedOutput('00');
461
});
462
463
+ // @gate !variant
464
test('uses the latest getSnapshot, even if it changed in the same batch as a store update', () => {
465
const store = createExternalStore({a: 0, b: 0});
466
@@ -436,7 +475,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
475
return <Text text={text} />;
476
}
477
439
- const root = createRoot(<App />);
478
+ const root = createRoot();
479
+ act(() => root.render(<App />));
480
expect(Scheduler).toHaveYielded([0]);
481
482
// Update the store and getSnapshot at the same time
@@ -449,6 +489,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
489
expect(root).toMatchRenderedOutput('2');
490
});
491
492
+ // @gate !variant
493
test('handles errors thrown by getSnapshot or isEqual', () => {
494
class ErrorBoundary extends React.Component {
495
state = {error: null};
@@ -492,10 +533,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
533
}
534
535
const errorBoundary = React.createRef(null);
495
- const root = createRoot(
496
- <ErrorBoundary ref={errorBoundary}>
497
- <App />
498
- </ErrorBoundary>,
536
+ const root = createRoot();
537
+ act(() =>
538
+ root.render(
539
+ <ErrorBoundary ref={errorBoundary}>
540
+ <App />
541
+ </ErrorBoundary>,
542
+ ),
543
);
544
expect(Scheduler).toHaveYielded([0]);
545
expect(root).toMatchRenderedOutput('0');
@@ -524,7 +568,32 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
568
expect(root).toMatchRenderedOutput('1');
569
});
570
571
+ // @gate !variant
572
+ test('Infinite loop if getSnapshot keeps returning new reference', () => {
573
+ const store = createExternalStore({});
574
+
575
+ function App() {
576
+ const text = useSyncExternalStore(store.subscribe, () => ({}));
577
+ return <Text text={JSON.stringify(text)} />;
578
+ }
579
+
580
+ spyOnDev(console, 'error');
581
+ const root = createRoot();
582
+
583
+ expect(() => act(() => root.render(<App />))).toThrow(
584
+ 'Maximum update depth exceeded. This can happen when a component repeatedly ' +
585
+ 'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
586
+ 'the number of nested updates to prevent infinite loops.',
587
+ );
588
+ if (__DEV__) {
589
+ expect(console.error.calls.argsFor(0)[0]).toMatch(
590
+ 'The result of getSnapshot should be cached to avoid an infinite loop',
591
+ );
592
+ }
593
+ });
594
+
595
describe('extra features implemented in user-space', () => {
596
+ // @gate !variant
597
test('memoized selectors are only called once per update', () => {
598
const store = createExternalStore({a: 0, b: 0});
599
@@ -543,7 +612,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
612
return <Text text={'A' + a} />;
613
}
614
546
- const root = createRoot(<App />);
615
+ const root = createRoot();
616
+ act(() => root.render(<App />));
617
618
expect(Scheduler).toHaveYielded(['App', 'Selector', 'A0']);
619
expect(root).toMatchRenderedOutput('A0');
@@ -563,6 +633,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
633
expect(root).toMatchRenderedOutput('A1');
634
});
635
636
+ // @gate !variant
637
test('Using isEqual to bailout', () => {
638
const store = createExternalStore({a: 0, b: 0});
639
@@ -596,7 +667,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
667
);
668
}
669
599
- const root = createRoot(<App />);
670
+ const root = createRoot();
671
+ act(() => root.render(<App />));
672
673
expect(Scheduler).toHaveYielded(['A0', 'B0']);
674
expect(root).toMatchRenderedOutput('A0B0');
@@ -618,30 +690,4 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
690
expect(root).toMatchRenderedOutput('A1B1');
691
});
692
});
621
-
622
- test('Infinite loop if getSnapshot keeps returning new reference', () => {
623
- const store = createExternalStore({});
624
-
625
- function App() {
626
- const text = useSyncExternalStore(store.subscribe, () => ({}));
627
- return <Text text={JSON.stringify(text)} />;
628
- }
629
-
630
- spyOnDev(console, 'error');
631
-
632
- expect(() => {
633
- act(() => {
634
- createRoot(<App />);
635
- });
636
- }).toThrow(
637
- 'Maximum update depth exceeded. This can happen when a component repeatedly ' +
638
- 'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
639
- 'the number of nested updates to prevent infinite loops.',
640
- );
641
- if (__DEV__) {
642
- expect(console.error.calls.argsFor(0)[0]).toMatch(
643
- 'The result of getSnapshot should be cached to avoid an infinite loop',
644
- );
645
- }
646
- });
693
});
packages/use-sync-external-store/src/useSyncExternalStore.js
+3
-3
@@ -17,8 +17,6 @@ const {
17
useEffect,
18
useLayoutEffect,
19
useDebugValue,
20
-
21
- // $FlowFixMe - useSyncExternalStore not yet part of React Flow types
20
useSyncExternalStore: builtInAPI,
21
} = React;
22
@@ -26,7 +24,9 @@ const {
24
// we're in version 16 or 17, so rendering is always synchronous. The shim
25
// does not support concurrent rendering, only the built-in API.
26
export const useSyncExternalStore =
29
- builtInAPI !== undefined ? builtInAPI : useSyncExternalStore_shim;
27
+ builtInAPI !== undefined
28
+ ? ((builtInAPI: any): typeof useSyncExternalStore_shim)
29
+ : useSyncExternalStore_shim;
30
31
let didWarnOld18Alpha = false;
32
let didWarnUncachedGetSnapshot = false;
scripts/jest/TestFlags.js
+2
@@ -42,6 +42,8 @@ const environmentFlags = {
42
// Similarly, should stable imply "classic"?
43
stable: !__EXPERIMENTAL__,
44
45
+ variant: __VARIANT__,
46
+
47
persistent: global.__PERSISTENT__ === true,
48
49
// Use this for tests that are known to be broken.