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.
This commit is contained in:
Andrew Clark
2021-09-07 10:20:24 -07:00
committed by GitHub
parent 3bbc914d6f
commit 4756f74405
17 changed files with 311 additions and 61 deletions
+8
View File
@@ -265,6 +265,13 @@ function useMutableSource<Source, Snapshot>(
return value;
}
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function useTransition(): [boolean, (() => void) => void] {
// useTransition() composes multiple hooks internally.
// Advance the current hook index the same number of times
@@ -326,6 +333,7 @@ const Dispatcher: DispatcherType = {
useState,
useTransition,
useMutableSource,
useSyncExternalStore,
useDeferredValue,
useOpaqueIdentifier,
};
@@ -462,6 +462,13 @@ function useMutableSource<Source, Snapshot>(
return getSnapshot(source._source);
}
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function useDeferredValue<T>(value: T): T {
resolveCurrentlyRenderingComponent();
return value;
@@ -514,6 +521,7 @@ export const Dispatcher: DispatcherType = {
useOpaqueIdentifier,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,
};
if (enableCache) {
@@ -1242,6 +1242,20 @@ function updateMutableSource<Source, Snapshot>(
return useMutableSource(hook, source, getSnapshot, subscribe);
}
function mountSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
@@ -2079,6 +2093,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
@@ -2104,6 +2119,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2129,6 +2145,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2154,6 +2171,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2302,6 +2320,14 @@ if (__DEV__) {
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
mountHookTypesDev();
@@ -2426,6 +2452,14 @@ if (__DEV__) {
updateHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2550,6 +2584,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2675,6 +2717,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2813,6 +2863,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
@@ -2952,6 +3011,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
@@ -3092,6 +3160,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
@@ -1242,6 +1242,20 @@ function updateMutableSource<Source, Snapshot>(
return useMutableSource(hook, source, getSnapshot, subscribe);
}
function mountSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
@@ -2079,6 +2093,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
@@ -2104,6 +2119,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2129,6 +2145,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2154,6 +2171,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
@@ -2302,6 +2320,14 @@ if (__DEV__) {
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
mountHookTypesDev();
@@ -2426,6 +2452,14 @@ if (__DEV__) {
updateHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2550,6 +2584,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2675,6 +2717,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
updateHookTypesDev();
@@ -2813,6 +2863,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
@@ -2952,6 +3011,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
@@ -3092,6 +3160,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
warnInvalidHookAccess();
+5
View File
@@ -41,6 +41,7 @@ export type HookType =
| 'useDeferredValue'
| 'useTransition'
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useOpaqueIdentifier'
| 'useCacheRefresh';
@@ -304,6 +305,10 @@ export type Dispatcher = {|
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot,
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T,
useOpaqueIdentifier(): any,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
+8
View File
@@ -461,6 +461,13 @@ function useMutableSource<Source, Snapshot>(
return getSnapshot(source._source);
}
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
throw new Error('Not yet implemented');
}
function useDeferredValue<T>(value: T): T {
resolveCurrentlyRenderingComponent();
return value;
@@ -509,6 +516,7 @@ export const Dispatcher: DispatcherType = {
useOpaqueIdentifier,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,
};
if (enableCache) {
+1
View File
@@ -827,6 +827,7 @@ const Dispatcher: DispatcherType = {
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
useSyncExternalStore: (unsupportedHook: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
},
@@ -44,6 +44,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
useTransition: unsupported,
useOpaqueIdentifier: unsupported,
useMutableSource: unsupported,
useSyncExternalStore: unsupported,
useCacheRefresh: unsupported,
};
// Not using async/await because we don't compile it.
+2
View File
@@ -51,6 +51,8 @@ export {
useMemo,
useMutableSource,
useMutableSource as unstable_useMutableSource,
useSyncExternalStore,
useSyncExternalStore as unstable_useSyncExternalStore,
useReducer,
useRef,
useState,
+1
View File
@@ -45,6 +45,7 @@ export {
useLayoutEffect,
useMemo,
useMutableSource as unstable_useMutableSource,
useSyncExternalStore as unstable_useSyncExternalStore,
useReducer,
useRef,
useState,
+2
View File
@@ -70,6 +70,8 @@ export {
useLayoutEffect,
useMemo,
useMutableSource,
useSyncExternalStore,
useSyncExternalStore as unstable_useSyncExternalStore,
useReducer,
useRef,
useState,
+2
View File
@@ -50,6 +50,8 @@ export {
useMemo,
useMutableSource,
useMutableSource as unstable_useMutableSource,
useSyncExternalStore,
useSyncExternalStore as unstable_useSyncExternalStore,
useReducer,
useRef,
useState,
+2
View File
@@ -44,6 +44,7 @@ import {
useLayoutEffect,
useMemo,
useMutableSource,
useSyncExternalStore,
useReducer,
useRef,
useState,
@@ -93,6 +94,7 @@ export {
useLayoutEffect,
useMemo,
useMutableSource,
useSyncExternalStore,
useReducer,
useRef,
useState,
+8
View File
@@ -169,6 +169,14 @@ export function useMutableSource<Source, Snapshot>(
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
}
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useSyncExternalStore(subscribe, getSnapshot);
}
export function useCacheRefresh(): <T>(?() => T, ?T) => void {
const dispatcher = resolveDispatcher();
// $FlowFixMe This is unstable, thus optional
@@ -29,11 +29,28 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// use the shim.
// TODO: Don't do this during a variant test run. That way these tests run
// against both the shim and the built-in implementation.
jest.mock('react', () => {
// eslint-disable-next-line no-unused-vars
const {startTransition, ...otherExports} = jest.requireActual('react');
return otherExports;
});
if (gate(flags => flags.variant)) {
// We'll use the variant flag to represent the native implementation
} else {
// and the non-variant tests for the shim.
//
// Remove useSyncExternalStore from the React imports so that we use the
// shim instead. Also removing startTransition, since we use that to
// detect outdated 18 alphas that don't yet include useSyncExternalStore.
//
// Longer term, we'll probably test this branch using an actual build
// of React 17.
jest.mock('react', () => {
const {
// eslint-disable-next-line no-unused-vars
startTransition: _,
// eslint-disable-next-line no-unused-vars
useSyncExternalStore: __,
...otherExports
} = jest.requireActual('react');
return otherExports;
});
}
React = require('react');
ReactNoop = require('react-noop-renderer');
@@ -63,14 +80,17 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
function createRoot(element) {
// This wrapper function exists so we can test both legacy roots and
// concurrent roots.
//
// TODO: Once the built-in API exists, conditionally test the concurrent
// root API, too.
const root = ReactNoop.createLegacyRoot();
act(() => {
root.render(element);
});
return root;
if (gate(flags => flags.variant)) {
// The native implementation only exists in 18+, so we test using
// concurrent mode. To test the legacy root behavior in the native
// implementation (which is supported in the sense that it needs to have
// the correct behavior, despite the fact that the legacy root API
// triggers a warning in 18), write a test that uses
// createLegacyRoot directly.
return ReactNoop.createRoot();
} else {
return ReactNoop.createLegacyRoot();
}
}
function createExternalStore(initialState) {
@@ -96,6 +116,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
};
}
// @gate !variant
test('basic usage', () => {
const store = createExternalStore('Initial');
@@ -104,7 +125,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={text} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['Initial']);
expect(root).toMatchRenderedOutput('Initial');
@@ -116,6 +138,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('Updated');
});
// @gate !variant
test('skips re-rendering if nothing changes', () => {
const store = createExternalStore('Initial');
@@ -124,7 +147,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={text} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['Initial']);
expect(root).toMatchRenderedOutput('Initial');
@@ -138,6 +162,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('Initial');
});
// @gate !variant
test('switch to a different store', () => {
const storeA = createExternalStore(0);
const storeB = createExternalStore(0);
@@ -150,7 +175,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={value} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0]);
expect(root).toMatchRenderedOutput('0');
@@ -187,6 +213,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('1');
});
// @gate !variant
test('selecting a specific value inside getSnapshot', () => {
const store = createExternalStore({a: 0, b: 0});
@@ -208,7 +235,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
);
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A0', 'B0']);
expect(root).toMatchRenderedOutput('A0B0');
@@ -230,6 +258,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('A1B1');
});
// @gate !variant
test(
"compares to current state before bailing out, even when there's a " +
'mutation in between the sync and passive effects',
@@ -244,7 +273,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={value} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0, 'Passive effect: 0']);
// 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)', () => {
},
);
// @gate !variant
test('mutating the store in between render and commit when getSnapshot has changed', () => {
const store = createExternalStore({a: 1, b: 1});
@@ -312,7 +343,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
);
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A1']);
expect(root).toMatchRenderedOutput('A1');
@@ -330,6 +362,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('B2');
});
// @gate !variant
test('mutating the store in between render and commit when getSnapshot has _not_ changed', () => {
// Same as previous test, but `getSnapshot` does not change
const store = createExternalStore({a: 1, b: 1});
@@ -368,7 +401,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
);
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A1']);
expect(root).toMatchRenderedOutput('A1');
@@ -387,6 +421,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('A1');
});
// @gate !variant
test("does not bail out if the previous update hasn't finished yet", () => {
const store = createExternalStore(0);
@@ -406,11 +441,14 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={value} />;
}
const root = createRoot(
<>
<Child1 />
<Child2 />
</>,
const root = createRoot();
act(() =>
root.render(
<>
<Child1 />
<Child2 />
</>,
),
);
expect(Scheduler).toHaveYielded([0, 0]);
expect(root).toMatchRenderedOutput('00');
@@ -422,6 +460,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('00');
});
// @gate !variant
test('uses the latest getSnapshot, even if it changed in the same batch as a store update', () => {
const store = createExternalStore({a: 0, b: 0});
@@ -436,7 +475,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={text} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded([0]);
// Update the store and getSnapshot at the same time
@@ -449,6 +489,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('2');
});
// @gate !variant
test('handles errors thrown by getSnapshot or isEqual', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
@@ -492,10 +533,13 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
}
const errorBoundary = React.createRef(null);
const root = createRoot(
<ErrorBoundary ref={errorBoundary}>
<App />
</ErrorBoundary>,
const root = createRoot();
act(() =>
root.render(
<ErrorBoundary ref={errorBoundary}>
<App />
</ErrorBoundary>,
),
);
expect(Scheduler).toHaveYielded([0]);
expect(root).toMatchRenderedOutput('0');
@@ -524,7 +568,32 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('1');
});
// @gate !variant
test('Infinite loop if getSnapshot keeps returning new reference', () => {
const store = createExternalStore({});
function App() {
const text = useSyncExternalStore(store.subscribe, () => ({}));
return <Text text={JSON.stringify(text)} />;
}
spyOnDev(console, 'error');
const root = createRoot();
expect(() => act(() => root.render(<App />))).toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
if (__DEV__) {
expect(console.error.calls.argsFor(0)[0]).toMatch(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
}
});
describe('extra features implemented in user-space', () => {
// @gate !variant
test('memoized selectors are only called once per update', () => {
const store = createExternalStore({a: 0, b: 0});
@@ -543,7 +612,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
return <Text text={'A' + a} />;
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['App', 'Selector', 'A0']);
expect(root).toMatchRenderedOutput('A0');
@@ -563,6 +633,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('A1');
});
// @gate !variant
test('Using isEqual to bailout', () => {
const store = createExternalStore({a: 0, b: 0});
@@ -596,7 +667,8 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
);
}
const root = createRoot(<App />);
const root = createRoot();
act(() => root.render(<App />));
expect(Scheduler).toHaveYielded(['A0', 'B0']);
expect(root).toMatchRenderedOutput('A0B0');
@@ -618,30 +690,4 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
expect(root).toMatchRenderedOutput('A1B1');
});
});
test('Infinite loop if getSnapshot keeps returning new reference', () => {
const store = createExternalStore({});
function App() {
const text = useSyncExternalStore(store.subscribe, () => ({}));
return <Text text={JSON.stringify(text)} />;
}
spyOnDev(console, 'error');
expect(() => {
act(() => {
createRoot(<App />);
});
}).toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
if (__DEV__) {
expect(console.error.calls.argsFor(0)[0]).toMatch(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
}
});
});
@@ -17,8 +17,6 @@ const {
useEffect,
useLayoutEffect,
useDebugValue,
// $FlowFixMe - useSyncExternalStore not yet part of React Flow types
useSyncExternalStore: builtInAPI,
} = React;
@@ -26,7 +24,9 @@ const {
// we're in version 16 or 17, so rendering is always synchronous. The shim
// does not support concurrent rendering, only the built-in API.
export const useSyncExternalStore =
builtInAPI !== undefined ? builtInAPI : useSyncExternalStore_shim;
builtInAPI !== undefined
? ((builtInAPI: any): typeof useSyncExternalStore_shim)
: useSyncExternalStore_shim;
let didWarnOld18Alpha = false;
let didWarnUncachedGetSnapshot = false;
+2
View File
@@ -42,6 +42,8 @@ const environmentFlags = {
// Similarly, should stable imply "classic"?
stable: !__EXPERIMENTAL__,
variant: __VARIANT__,
persistent: global.__PERSISTENT__ === true,
// Use this for tests that are known to be broken.