main
js 132 lines 4.56 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import * as React from 'react';
11 import is from 'shared/objectIs';
12 import {useSyncExternalStore} from 'use-sync-external-store/src/useSyncExternalStore';
13
14 // Intentionally not using named imports because Rollup uses dynamic dispatch
15 // for CommonJS interop.
16 const {useRef, useEffect, useMemo, useDebugValue} = React;
17
18 // Same as useSyncExternalStore, but supports selector and isEqual arguments.
19 export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
20 subscribe: (() => void) => () => void,
21 getSnapshot: () => Snapshot,
22 getServerSnapshot: void | null | (() => Snapshot),
23 selector: (snapshot: Snapshot) => Selection,
24 isEqual?: (a: Selection, b: Selection) => boolean,
25 ): Selection {
26 type Inst =
27 | {
28 hasValue: true,
29 value: Selection,
30 }
31 | {
32 hasValue: false,
33 value: null,
34 };
35
36 // Use this to track the rendered snapshot.
37 const instRef = useRef<Inst | null>(null);
38 let inst: Inst;
39 if (instRef.current === null) {
40 inst = {
41 hasValue: false,
42 value: null,
43 };
44 instRef.current = inst;
45 } else {
46 inst = instRef.current;
47 }
48
49 const [getSelection, getServerSelection] = useMemo(() => {
50 // Track the memoized state using closure variables that are local to this
51 // memoized instance of a getSnapshot function. Intentionally not using a
52 // useRef hook, because that state would be shared across all concurrent
53 // copies of the hook/component.
54 let hasMemo = false;
55 let memoizedSnapshot;
56 let memoizedSelection: Selection;
57 const memoizedSelector = (nextSnapshot: Snapshot) => {
58 if (!hasMemo) {
59 // The first time the hook is called, there is no memoized result.
60 hasMemo = true;
61 memoizedSnapshot = nextSnapshot;
62 const nextSelection = selector(nextSnapshot);
63 if (isEqual !== undefined) {
64 // Even if the selector has changed, the currently rendered selection
65 // may be equal to the new selection. We should attempt to reuse the
66 // current value if possible, to preserve downstream memoizations.
67 if (inst.hasValue) {
68 const currentSelection = inst.value;
69 if (isEqual(currentSelection, nextSelection)) {
70 memoizedSelection = currentSelection;
71 return currentSelection;
72 }
73 }
74 }
75 memoizedSelection = nextSelection;
76 return nextSelection;
77 }
78
79 // We may be able to reuse the previous invocation's result.
80 const prevSnapshot: Snapshot = memoizedSnapshot as any;
81 const prevSelection: Selection = memoizedSelection as any;
82
83 if (is(prevSnapshot, nextSnapshot)) {
84 // The snapshot is the same as last time. Reuse the previous selection.
85 return prevSelection;
86 }
87
88 // The snapshot has changed, so we need to compute a new selection.
89 const nextSelection = selector(nextSnapshot);
90
91 // If a custom isEqual function is provided, use that to check if the data
92 // has changed. If it hasn't, return the previous selection. That signals
93 // to React that the selections are conceptually equal, and we can bail
94 // out of rendering.
95 if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
96 // The snapshot still has changed, so make sure to update to not keep
97 // old references alive
98 memoizedSnapshot = nextSnapshot;
99 return prevSelection;
100 }
101
102 memoizedSnapshot = nextSnapshot;
103 memoizedSelection = nextSelection;
104 return nextSelection;
105 };
106 // Assigning this to a constant so that Flow knows it can't change.
107 const maybeGetServerSnapshot =
108 getServerSnapshot === undefined ? null : getServerSnapshot;
109 const getSnapshotWithSelector = () => memoizedSelector(getSnapshot());
110 const getServerSnapshotWithSelector =
111 maybeGetServerSnapshot === null
112 ? undefined
113 : () => memoizedSelector(maybeGetServerSnapshot());
114 return [getSnapshotWithSelector, getServerSnapshotWithSelector];
115 }, [getSnapshot, getServerSnapshot, selector, isEqual]);
116
117 const value = useSyncExternalStore(
118 subscribe,
119 getSelection,
120 getServerSelection,
121 );
122
123 useEffect(() => {
124 // $FlowFixMe[incompatible-type] changing the variant using mutation isn't supported
125 inst.hasValue = true;
126 // $FlowFixMe[incompatible-type]
127 inst.value = value;
128 }, [value]);
129
130 useDebugValue(value);
131 return value;
132 }