mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
c5eca9b082
## Summary <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> A proposed fix for the bug described in https://github.com/facebook/react/issues/25967 ## How did you test this change? See the issue linked above, test scenario included in the code sandbox: https://codesandbox.io/s/fervent-ives-0vm9es?file=/src/App.jsx <!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. How exactly did you verify that your PR solves the issue you wanted to solve? If you leave this empty, your PR will very likely be closed. -->
133 lines
4.5 KiB
JavaScript
133 lines
4.5 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import * as React from 'react';
|
|
import is from 'shared/objectIs';
|
|
import {useSyncExternalStore} from 'use-sync-external-store/src/useSyncExternalStore';
|
|
|
|
// Intentionally not using named imports because Rollup uses dynamic dispatch
|
|
// for CommonJS interop.
|
|
const {useRef, useEffect, useMemo, useDebugValue} = React;
|
|
|
|
// Same as useSyncExternalStore, but supports selector and isEqual arguments.
|
|
export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
|
|
subscribe: (() => void) => () => void,
|
|
getSnapshot: () => Snapshot,
|
|
getServerSnapshot: void | null | (() => Snapshot),
|
|
selector: (snapshot: Snapshot) => Selection,
|
|
isEqual?: (a: Selection, b: Selection) => boolean,
|
|
): Selection {
|
|
// Use this to track the rendered snapshot.
|
|
const instRef = useRef<
|
|
| {
|
|
hasValue: true,
|
|
value: Selection,
|
|
}
|
|
| {
|
|
hasValue: false,
|
|
value: null,
|
|
}
|
|
| null,
|
|
>(null);
|
|
let inst;
|
|
if (instRef.current === null) {
|
|
inst = {
|
|
hasValue: false,
|
|
value: null,
|
|
};
|
|
instRef.current = inst;
|
|
} else {
|
|
inst = instRef.current;
|
|
}
|
|
|
|
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
|
|
// copies of the hook/component.
|
|
let hasMemo = false;
|
|
let memoizedSnapshot;
|
|
let memoizedSelection: Selection;
|
|
const memoizedSelector = (nextSnapshot: Snapshot) => {
|
|
if (!hasMemo) {
|
|
// The first time the hook is called, there is no memoized result.
|
|
hasMemo = true;
|
|
memoizedSnapshot = nextSnapshot;
|
|
const nextSelection = selector(nextSnapshot);
|
|
if (isEqual !== undefined) {
|
|
// Even if the selector has changed, the currently rendered selection
|
|
// may be equal to the new selection. We should attempt to reuse the
|
|
// current value if possible, to preserve downstream memoizations.
|
|
if (inst.hasValue) {
|
|
const currentSelection = inst.value;
|
|
if (isEqual(currentSelection, nextSelection)) {
|
|
memoizedSelection = currentSelection;
|
|
return currentSelection;
|
|
}
|
|
}
|
|
}
|
|
memoizedSelection = nextSelection;
|
|
return nextSelection;
|
|
}
|
|
|
|
// We may be able to reuse the previous invocation's result.
|
|
const prevSnapshot: Snapshot = (memoizedSnapshot: any);
|
|
const prevSelection: Selection = (memoizedSelection: any);
|
|
|
|
if (is(prevSnapshot, nextSnapshot)) {
|
|
// The snapshot is the same as last time. Reuse the previous selection.
|
|
return prevSelection;
|
|
}
|
|
|
|
// The snapshot has changed, so we need to compute a new selection.
|
|
const nextSelection = selector(nextSnapshot);
|
|
|
|
// If a custom isEqual function is provided, use that to check if the data
|
|
// has changed. If it hasn't, return the previous selection. That signals
|
|
// to React that the selections are conceptually equal, and we can bail
|
|
// out of rendering.
|
|
if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
|
|
// The snapshot still has changed, so make sure to update to not keep
|
|
// old references alive
|
|
memoizedSnapshot = nextSnapshot;
|
|
return prevSelection;
|
|
}
|
|
|
|
memoizedSnapshot = nextSnapshot;
|
|
memoizedSelection = nextSelection;
|
|
return nextSelection;
|
|
};
|
|
// 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,
|
|
getSelection,
|
|
getServerSelection,
|
|
);
|
|
|
|
useEffect(() => {
|
|
// $FlowFixMe[incompatible-type] changing the variant using mutation isn't supported
|
|
inst.hasValue = true;
|
|
// $FlowFixMe[incompatible-type]
|
|
inst.value = value;
|
|
}, [value]);
|
|
|
|
useDebugValue(value);
|
|
return value;
|
|
}
|