|
1 | 1 | // Simplified fork of the useSyncExternalStore hook from the official package, |
2 | 2 | // with `getServerSnapshot` parameter removed and `instRef` simplified using a symbol. |
3 | | -// https://github.com/facebook/react/blob/8d74e8c73a5cc5e461bb1413a74c6b058c6be134/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js |
| 3 | +// https://github.com/facebook/react/blob/58bdc0bb967098f14562cd76af0668f2056459a0/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js |
4 | 4 | // MIT Licensed https://github.com/facebook/react/blob/be8aa76873e231555676483a36534bb48ad1b1a3/LICENSE |
5 | 5 |
|
6 | | -import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react' |
| 6 | +/* eslint @stylistic/semi: ["error", "always"] */ |
| 7 | +/* eslint-disable react-hooks/immutability */ |
7 | 8 |
|
8 | | -const NIL = Symbol('NIL') |
| 9 | +import { useDebugValue, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; |
| 10 | + |
| 11 | +const NIL = Symbol('NIL'); |
9 | 12 |
|
10 | 13 | export function useSyncExternalStoreWithSelector<Snapshot, Selection>( |
11 | 14 | subscribe: (onStoreChange: () => void) => () => void, |
12 | 15 | getSnapshot: () => Snapshot, |
13 | 16 | selector: (snapshot: Snapshot) => Selection, |
14 | | - isEqual: (a: Selection, b: Selection) => boolean = Object.is, |
| 17 | + isEqual?: (a: Selection, b: Selection) => boolean, |
15 | 18 | ): Selection { |
16 | 19 | // Use this to track the rendered snapshot. |
17 | | - const instRef = useRef<Selection | typeof NIL>(NIL) |
| 20 | + const instRef = useRef<Selection | typeof NIL>(NIL); |
18 | 21 |
|
19 | | - const getSelection = useMemo(() => { |
| 22 | + const [getSelection] = useMemo(() => { |
20 | 23 | // Track the memoized state using closure variables that are local to this |
21 | 24 | // memoized instance of a getSnapshot function. Intentionally not using a |
22 | 25 | // useRef hook, because that state would be shared across all concurrent |
23 | 26 | // copies of the hook/component. |
24 | | - let hasMemo = false |
25 | | - let memoizedSnapshot: Snapshot |
26 | | - let memoizedSelection: Selection |
| 27 | + let hasMemo = false; |
| 28 | + let memoizedSnapshot: Snapshot; |
| 29 | + let memoizedSelection: Selection; |
27 | 30 | const memoizedSelector = (nextSnapshot: Snapshot) => { |
28 | 31 | if (!hasMemo) { |
29 | 32 | // The first time the hook is called, there is no memoized result. |
30 | | - // eslint-disable-next-line react-hooks/immutability |
31 | | - hasMemo = true |
32 | | - memoizedSnapshot = nextSnapshot |
33 | | - const nextSelection = selector(nextSnapshot) |
34 | | - // Even if the selector has changed, the currently rendered selection |
35 | | - // may be equal to the new selection. We should attempt to reuse the |
36 | | - // current value if possible, to preserve downstream memoizations. |
37 | | - if (instRef.current !== NIL) { |
38 | | - const currentSelection = instRef.current |
39 | | - if (isEqual(currentSelection, nextSelection)) { |
40 | | - memoizedSelection = currentSelection |
41 | | - return currentSelection |
| 33 | + hasMemo = true; |
| 34 | + memoizedSnapshot = nextSnapshot; |
| 35 | + const nextSelection = selector(nextSnapshot); |
| 36 | + if (isEqual !== undefined) { |
| 37 | + // Even if the selector has changed, the currently rendered selection |
| 38 | + // may be equal to the new selection. We should attempt to reuse the |
| 39 | + // current value if possible, to preserve downstream memoizations. |
| 40 | + if (instRef.current !== NIL) { |
| 41 | + const currentSelection = instRef.current; |
| 42 | + if (isEqual(currentSelection, nextSelection)) { |
| 43 | + memoizedSelection = currentSelection; |
| 44 | + return currentSelection; |
| 45 | + } |
42 | 46 | } |
43 | 47 | } |
44 | | - memoizedSelection = nextSelection |
45 | | - return nextSelection |
| 48 | + memoizedSelection = nextSelection; |
| 49 | + return nextSelection; |
46 | 50 | } |
47 | 51 |
|
48 | 52 | // We may be able to reuse the previous invocation's result. |
49 | | - const prevSnapshot = memoizedSnapshot |
50 | | - const prevSelection = memoizedSelection |
| 53 | + const prevSnapshot = memoizedSnapshot; |
| 54 | + const prevSelection = memoizedSelection; |
51 | 55 |
|
52 | 56 | if (Object.is(prevSnapshot, nextSnapshot)) { |
53 | 57 | // The snapshot is the same as last time. Reuse the previous selection. |
54 | | - return prevSelection |
| 58 | + return prevSelection; |
55 | 59 | } |
56 | 60 |
|
57 | 61 | // The snapshot has changed, so we need to compute a new selection. |
58 | | - const nextSelection = selector(nextSnapshot) |
| 62 | + const nextSelection = selector(nextSnapshot); |
59 | 63 |
|
60 | 64 | // If a custom isEqual function is provided, use that to check if the data |
61 | 65 | // has changed. If it hasn't, return the previous selection. That signals |
62 | 66 | // to React that the selections are conceptually equal, and we can bail |
63 | 67 | // out of rendering. |
64 | | - if (isEqual(prevSelection, nextSelection)) { |
| 68 | + if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) { |
65 | 69 | // The snapshot still has changed, so make sure to update to not keep |
66 | 70 | // old references alive |
67 | | - memoizedSnapshot = nextSnapshot |
68 | | - return prevSelection |
| 71 | + memoizedSnapshot = nextSnapshot; |
| 72 | + return prevSelection; |
69 | 73 | } |
70 | 74 |
|
71 | | - memoizedSnapshot = nextSnapshot |
72 | | - memoizedSelection = nextSelection |
73 | | - return nextSelection |
74 | | - } |
75 | | - |
76 | | - const getSnapshotWithSelector = () => memoizedSelector(getSnapshot()) |
77 | | - return getSnapshotWithSelector |
78 | | - }, [getSnapshot, selector, isEqual]) |
| 75 | + memoizedSnapshot = nextSnapshot; |
| 76 | + memoizedSelection = nextSelection; |
| 77 | + return nextSelection; |
| 78 | + }; |
| 79 | + const getSnapshotWithSelector = () => memoizedSelector(getSnapshot()); |
| 80 | + return [getSnapshotWithSelector]; |
| 81 | + }, [getSnapshot, selector, isEqual]); |
79 | 82 |
|
80 | | - const selection = useSyncExternalStore(subscribe, getSelection) |
| 83 | + const value = useSyncExternalStore( |
| 84 | + subscribe, |
| 85 | + getSelection, |
| 86 | + ); |
81 | 87 |
|
82 | 88 | useEffect(() => { |
83 | | - instRef.current = selection |
84 | | - }, [selection]) |
| 89 | + instRef.current = value; |
| 90 | + }, [value]); |
85 | 91 |
|
86 | | - return selection |
| 92 | + useDebugValue(value); |
| 93 | + return value; |
87 | 94 | } |
0 commit comments