@samitouri / QOS-React-2 / commits / 77912d9a05

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.