mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -51,6 +51,8 @@ export {
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -45,6 +45,7 @@ export {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -70,6 +70,8 @@ export {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -50,6 +50,8 @@ export {
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -93,6 +94,7 @@ export {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -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
|
||||
|
||||
+104
-58
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user