Implement useSyncExternalStore on server (#22347)

Adds a third argument called `getServerSnapshot`.

On the server, React calls this one instead of the normal `getSnapshot`.
We also call it during hydration.

So it represents the snapshot that is used to generate the initial,
server-rendered HTML. The purpose is to avoid server-client mismatches.
What we render during hydration needs to match up exactly with what we
render on the server.

The pattern is for the server to send down a serialized copy of the
store that was used to generate the initial HTML. On the client, React
will call either `getSnapshot` or `getServerSnapshot` on the client as
appropriate, depending on whether it's currently hydrating.

The argument is optional for fully client rendered use cases. If the
user does attempt to omit `getServerSnapshot`, and the hook is called
on the server, React will abort that subtree on the server and
revert to client rendering, up to the nearest Suspense boundary.

For the userspace shim, we will need to use a heuristic (canUseDOM)
to determine whether we are in a server environment. I'll do that in
a follow up.
This commit is contained in:
Andrew Clark 2021-09-20 11:31:02 -04:00 committed by GitHub
parent 57e4d6872f
commit 86b3e2461d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 341 additions and 71 deletions

View File

@ -258,6 +258,7 @@ function useMemo<T>(
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
// useSyncExternalStore() composes multiple hooks internally.
// Advance the current hook index the same number of times

View File

@ -17,6 +17,8 @@ let ReactDOM;
let ReactDOMFizzServer;
let Suspense;
let SuspenseList;
let useSyncExternalStore;
let useSyncExternalStoreExtra;
let PropTypes;
let textCache;
let document;
@ -39,6 +41,9 @@ describe('ReactDOMFizzServer', () => {
Stream = require('stream');
Suspense = React.Suspense;
SuspenseList = React.SuspenseList;
useSyncExternalStore = React.unstable_useSyncExternalStore;
useSyncExternalStoreExtra = require('use-sync-external-store/extra')
.useSyncExternalStoreExtra;
PropTypes = require('prop-types');
textCache = new Map();
@ -1478,4 +1483,156 @@ describe('ReactDOMFizzServer', () => {
// We should've been able to display the content without waiting for the rest of the fallback.
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});
// @gate supportsNativeUseSyncExternalStore
// @gate experimental
it('calls getServerSnapshot instead of getSnapshot', async () => {
const ref = React.createRef();
function getServerSnapshot() {
return 'server';
}
function getClientSnapshot() {
return 'client';
}
function subscribe() {
return () => {};
}
function Child({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
function App() {
const value = useSyncExternalStore(
subscribe,
getClientSnapshot,
getServerSnapshot,
);
return (
<div ref={ref}>
<Child text={value} />
</div>
);
}
const loggedErrors = [];
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback="Loading...">
<App />
</Suspense>,
writable,
{
onError(x) {
loggedErrors.push(x);
},
},
);
startWriting();
});
expect(Scheduler).toHaveYielded(['server']);
const serverRenderedDiv = container.getElementsByTagName('div')[0];
ReactDOM.hydrateRoot(container, <App />);
// The first paint uses the server snapshot
expect(Scheduler).toFlushUntilNextPaint(['server']);
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
// Hydration succeeded
expect(ref.current).toEqual(serverRenderedDiv);
// Asynchronously we detect that the store has changed on the client,
// and patch up the inconsistency
expect(Scheduler).toFlushUntilNextPaint(['client']);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(ref.current).toEqual(serverRenderedDiv);
});
// The selector implementation uses the lazy ref initialization pattern
// @gate !(enableUseRefAccessWarning && __DEV__)
// @gate supportsNativeUseSyncExternalStore
// @gate experimental
it('calls getServerSnapshot instead of getSnapshot (with selector and isEqual)', async () => {
// Same as previous test, but with a selector that returns a complex object
// that is memoized with a custom `isEqual` function.
const ref = React.createRef();
function getServerSnapshot() {
return {env: 'server', other: 'unrelated'};
}
function getClientSnapshot() {
return {env: 'client', other: 'unrelated'};
}
function selector({env}) {
return {env};
}
function isEqual(a, b) {
return a.env === b.env;
}
function subscribe() {
return () => {};
}
function Child({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
function App() {
const {env} = useSyncExternalStoreExtra(
subscribe,
getClientSnapshot,
getServerSnapshot,
selector,
isEqual,
);
return (
<div ref={ref}>
<Child text={env} />
</div>
);
}
const loggedErrors = [];
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback="Loading...">
<App />
</Suspense>,
writable,
{
onError(x) {
loggedErrors.push(x);
},
},
);
startWriting();
});
expect(Scheduler).toHaveYielded(['server']);
const serverRenderedDiv = container.getElementsByTagName('div')[0];
ReactDOM.hydrateRoot(container, <App />);
// The first paint uses the server snapshot
expect(Scheduler).toFlushUntilNextPaint(['server']);
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
// Hydration succeeded
expect(ref.current).toEqual(serverRenderedDiv);
// Asynchronously we detect that the store has changed on the client,
// and patch up the inconsistency
expect(Scheduler).toFlushUntilNextPaint(['client']);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(ref.current).toEqual(serverRenderedDiv);
});
});

View File

@ -464,8 +464,16 @@ export function useCallback<T>(
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
throw new Error('Not yet implemented');
if (getServerSnapshot === undefined) {
invariant(
false,
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
return getServerSnapshot();
}
function useDeferredValue<T>(value: T): T {

View File

@ -938,23 +938,64 @@ function rerenderReducer<S, I, A>(
function mountSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
if (getServerSnapshot === undefined) {
invariant(
false,
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
nextSnapshot = getServerSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getServerSnapshot()) {
console.error(
'The result of getServerSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
} else {
nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getSnapshot()) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
// Unless we're rendering a blocking lane, schedule a consistency check.
// Right before committing, we will walk the tree and check if any of the
// stores were mutated.
//
// We won't do this if we're hydrating server-rendered content, because if
// the content is stale, it's already visible anyway. Instead we'll patch
// it up in a passive effect.
const root: FiberRoot | null = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
// Read the current snapshot from the store on every render. This breaks the
// normal rules of React, and only works because store updates are
// always synchronous.
const nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getSnapshot()) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
hook.memoizedState = nextSnapshot;
const inst: StoreInstance<T> = {
value: nextSnapshot,
@ -980,24 +1021,13 @@ function mountSyncExternalStore<T>(
null,
);
// Unless we're rendering a blocking lane, schedule a consistency check. Right
// before committing, we will walk the tree and check if any of the stores
// were mutated.
const root: FiberRoot | null = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
return nextSnapshot;
}
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = updateWorkInProgressHook();
@ -2235,10 +2265,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2366,10 +2397,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2497,10 +2529,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2629,10 +2662,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2774,11 +2808,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2921,11 +2956,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -3069,11 +3105,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';

View File

@ -938,23 +938,64 @@ function rerenderReducer<S, I, A>(
function mountSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
if (getServerSnapshot === undefined) {
invariant(
false,
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
nextSnapshot = getServerSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getServerSnapshot()) {
console.error(
'The result of getServerSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
} else {
nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getSnapshot()) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
// Unless we're rendering a blocking lane, schedule a consistency check.
// Right before committing, we will walk the tree and check if any of the
// stores were mutated.
//
// We won't do this if we're hydrating server-rendered content, because if
// the content is stale, it's already visible anyway. Instead we'll patch
// it up in a passive effect.
const root: FiberRoot | null = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
// Read the current snapshot from the store on every render. This breaks the
// normal rules of React, and only works because store updates are
// always synchronous.
const nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
if (nextSnapshot !== getSnapshot()) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
hook.memoizedState = nextSnapshot;
const inst: StoreInstance<T> = {
value: nextSnapshot,
@ -980,24 +1021,13 @@ function mountSyncExternalStore<T>(
null,
);
// Unless we're rendering a blocking lane, schedule a consistency check. Right
// before committing, we will walk the tree and check if any of the stores
// were mutated.
const root: FiberRoot | null = getWorkInProgressRoot();
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
return nextSnapshot;
}
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const fiber = currentlyRenderingFiber;
const hook = updateWorkInProgressHook();
@ -2235,10 +2265,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2366,10 +2397,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2497,10 +2529,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2629,10 +2662,11 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2774,11 +2808,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
mountHookTypesDev();
return mountSyncExternalStore(subscribe, getSnapshot);
return mountSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -2921,11 +2956,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';
@ -3069,11 +3105,12 @@ if (__DEV__) {
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
currentHookNameInDev = 'useSyncExternalStore';
warnInvalidHookAccess();
updateHookTypesDev();
return updateSyncExternalStore(subscribe, getSnapshot);
return updateSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
},
useOpaqueIdentifier(): OpaqueIDType | void {
currentHookNameInDev = 'useOpaqueIdentifier';

View File

@ -294,6 +294,7 @@ export type Dispatcher = {|
useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T,
useOpaqueIdentifier(): any,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,

View File

@ -447,8 +447,16 @@ export function useCallback<T>(
function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
throw new Error('Not yet implemented');
if (getServerSnapshot === undefined) {
invariant(
false,
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
return getServerSnapshot();
}
function useDeferredValue<T>(value: T): T {

View File

@ -166,9 +166,14 @@ export function useOpaqueIdentifier(): OpaqueIDType | void {
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useSyncExternalStore(subscribe, getSnapshot);
return dispatcher.useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
}
export function useCacheRefresh(): <T>(?() => T, ?T) => void {

View File

@ -588,6 +588,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const a = useSyncExternalStoreExtra(
store.subscribe,
store.getState,
null,
selector,
);
return <Text text={'A' + a} />;
@ -623,6 +624,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const {a} = useSyncExternalStoreExtra(
store.subscribe,
store.getState,
null,
state => ({a: state.a}),
(state1, state2) => state1.a === state2.a,
);
@ -632,6 +634,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const {b} = useSyncExternalStoreExtra(
store.subscribe,
store.getState,
null,
state => {
return {b: state.b};
},
@ -710,6 +713,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const items = useSyncExternalStoreExtra(
store.subscribe,
store.getState,
null,
inlineSelector,
shallowEqualArray,
);

View File

@ -45,6 +45,8 @@ let didWarnUncachedGetSnapshot = false;
function useSyncExternalStore_shim<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
// TODO: Add a canUseDOM check and use this one on the server
getServerSnapshot?: () => T,
): T {
if (__DEV__) {
if (!didWarnOld18Alpha) {
@ -95,7 +97,7 @@ function useSyncExternalStore_shim<T>(
// Track the latest getSnapshot function with a ref. This needs to be updated
// in the layout phase so we can access it during the tearing check that
// happens on subscribe.
// TODO: Circumvent SSR warning
// TODO: Circumvent SSR warning with canUseDOM check
useLayoutEffect(() => {
inst.value = value;
inst.getSnapshot = getSnapshot;

View File

@ -19,6 +19,7 @@ const {useRef, useEffect, useMemo, useDebugValue} = React;
export function useSyncExternalStoreExtra<Snapshot, Selection>(
subscribe: (() => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot: void | null | (() => Snapshot),
selector: (snapshot: Snapshot) => Selection,
isEqual?: (a: Selection, b: Selection) => boolean,
): Selection {
@ -35,7 +36,7 @@ export function useSyncExternalStoreExtra<Snapshot, Selection>(
inst = instRef.current;
}
const getSnapshotWithMemoizedSelector = useMemo(() => {
const [getSelection, getServerSelection] = useMemo(() => {
// Track the memoized state using closure variables that are local to this
// memoized instance of a getSnapshot function. Intentionally not using a
// useRef hook, because that state would be shared across all concurrent
@ -43,9 +44,7 @@ export function useSyncExternalStoreExtra<Snapshot, Selection>(
let hasMemo = false;
let memoizedSnapshot;
let memoizedSelection;
return () => {
const nextSnapshot = getSnapshot();
const memoizedSelector = nextSnapshot => {
if (!hasMemo) {
// The first time the hook is called, there is no memoized result.
hasMemo = true;
@ -91,11 +90,21 @@ export function useSyncExternalStoreExtra<Snapshot, Selection>(
memoizedSelection = nextSelection;
return nextSelection;
};
}, [getSnapshot, selector, isEqual]);
// Assigning this to a constant so that Flow knows it can't change.
const maybeGetServerSnapshot =
getServerSnapshot === undefined ? null : getServerSnapshot;
const getSnapshotWithSelector = () => memoizedSelector(getSnapshot());
const getServerSnapshotWithSelector =
maybeGetServerSnapshot === null
? undefined
: () => memoizedSelector(maybeGetServerSnapshot());
return [getSnapshotWithSelector, getServerSnapshotWithSelector];
}, [getSnapshot, getServerSnapshot, selector, isEqual]);
const value = useSyncExternalStore(
subscribe,
getSnapshotWithMemoizedSelector,
getSelection,
getServerSelection,
);
useEffect(() => {

View File

@ -394,5 +394,6 @@
"403": "Tried to pop a Context at the root of the app. This is a bug in React.",
"404": "Invalid hook call. Hooks can only be called inside of the body of a function component.",
"405": "hydrateRoot(...): Target container is not a DOM element.",
"406": "act(...) is not supported in production builds of React."
"406": "act(...) is not supported in production builds of React.",
"407": "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering."
}