/** * 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. * */ import { useRef, useEffect, useMemo, useDebugValue, useSyncExternalStore, } from 'react'; // NOTE: This is a port of https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js // and React's internal/shared version of `Object.is` with refactors for TypeScript and bundling. Doing this to avoid adding an additional dependency // beyond core React when using the `MediaStore` React hooks and related (CJP) /** * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is */ function isPolyfill(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare ); } type SnapshotRef = | { hasValue: true; value: Selection; } | { hasValue: false; value: null; } | null; const is: (x: any, y: any) => boolean = typeof Object.is === 'function' ? Object.is : isPolyfill; // Same as useSyncExternalStore, but supports selector and isEqual arguments. export function useSyncExternalStoreWithSelector( subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => Snapshot, getServerSnapshot: undefined | null | (() => Snapshot), selector: (snapshot: Snapshot) => Selection, isEqual?: (a: Selection, b: Selection) => boolean ) { // Use this to track the rendered snapshot. const instRef = useRef>(null); let inst: SnapshotRef; if (instRef.current === null) { inst = { hasValue: false, value: null, }; instRef.current = inst as SnapshotRef; } 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; const prevSelection: Selection = memoizedSelection; 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; }