mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
* Move useSyncExternalStore shim to a nested entrypoint Also renames `useSyncExternalStoreExtra` to `useSyncExternalStoreWithSelector`. - 'use-sync-external-store/shim' -> A shim for `useSyncExternalStore` that works in React 16 and 17 (any release that supports hooks). The module will first check if the built-in React API exists, before falling back to the shim. - 'use-sync-external-store/with-selector' -> An extended version of `useSyncExternalStore` that also supports `selector` and `isEqual` options. It does _not_ shim `use-sync-external-store`; it composes the built-in React API. **Use this if you only support 18+.** - 'use-sync-external-store/shim/with-selector' -> Same API, but it composes `use-sync-external-store/shim` instead. **Use this for compatibility with 16 and 17.** - 'use-sync-external-store' -> Re-exports React's built-in API. Not meant to be used. It will warn and direct users to either the shim or the built-in API. * Upgrade useSyncExternalStore to alpha channel
118 lines
4.1 KiB
JavaScript
118 lines
4.1 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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(null);
|
|
let inst;
|
|
if (instRef.current === null) {
|
|
inst = {
|
|
hasValue: false,
|
|
value: (null: Selection | 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;
|
|
const memoizedSelector = nextSnapshot => {
|
|
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)) {
|
|
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(() => {
|
|
inst.hasValue = true;
|
|
inst.value = value;
|
|
}, [value]);
|
|
|
|
useDebugValue(value);
|
|
return value;
|
|
}
|