Skip to content

Commit c31b8dd

Browse files
committed
Port uSESWS
1 parent a9129a5 commit c31b8dd

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import * as React from 'react'
2+
import is from '../utils/shallowEqual'
3+
import { useSyncExternalStore } from 'use-sync-external-store'
4+
5+
// Intentionally not using named imports because Rollup uses dynamic dispatch
6+
// for CommonJS interop.
7+
const { useRef, useEffect, useMemo, useDebugValue } = React
8+
// Same as useSyncExternalStore, but supports selector and isEqual arguments.
9+
export function useSyncExternalStoreWithSelector<Snapshot, Selection>(
10+
subscribe: (onStoreChange: () => void) => () => void,
11+
getSnapshot: () => Snapshot,
12+
getServerSnapshot: void | null | (() => Snapshot),
13+
selector: (snapshot: Snapshot) => Selection,
14+
isEqual?: (a: Selection, b: Selection) => boolean,
15+
): Selection {
16+
// Use this to track the rendered snapshot.
17+
const instRef = useRef<
18+
| {
19+
hasValue: true
20+
value: Selection
21+
}
22+
| {
23+
hasValue: false
24+
value: null
25+
}
26+
| null
27+
>(null)
28+
let inst
29+
30+
if (instRef.current === null) {
31+
inst = {
32+
hasValue: false,
33+
value: null,
34+
}
35+
// @ts-ignore
36+
instRef.current = inst
37+
} else {
38+
inst = instRef.current
39+
}
40+
41+
const [getSelection, getServerSelection] = useMemo(() => {
42+
// Track the memoized state using closure variables that are local to this
43+
// memoized instance of a getSnapshot function. Intentionally not using a
44+
// useRef hook, because that state would be shared across all concurrent
45+
// copies of the hook/component.
46+
let hasMemo = false
47+
let memoizedSnapshot: Snapshot
48+
let memoizedSelection: Selection
49+
let lastUsedProps: string[] = []
50+
let hasAccessed = false
51+
const accessedProps: string[] = []
52+
53+
const memoizedSelector = (nextSnapshot: Snapshot) => {
54+
const getProxy = (): Snapshot => {
55+
if (
56+
!(typeof nextSnapshot === 'object') ||
57+
typeof Proxy === 'undefined'
58+
) {
59+
return nextSnapshot
60+
}
61+
62+
const handler = {
63+
get: (target: Snapshot, prop: string, receiver: any) => {
64+
const propertyName = prop.toString()
65+
66+
if (accessedProps.indexOf(propertyName) === -1) {
67+
accessedProps.push(propertyName)
68+
}
69+
70+
const value = Reflect.get(target as any, prop, receiver)
71+
return value
72+
},
73+
}
74+
return new Proxy(nextSnapshot as any, handler) as any
75+
}
76+
77+
if (!hasMemo) {
78+
// The first time the hook is called, there is no memoized result.
79+
hasMemo = true
80+
memoizedSnapshot = nextSnapshot
81+
const nextSelection = selector(getProxy())
82+
lastUsedProps = accessedProps
83+
hasAccessed = true
84+
85+
if (isEqual !== undefined) {
86+
// Even if the selector has changed, the currently rendered selection
87+
// may be equal to the new selection. We should attempt to reuse the
88+
// current value if possible, to preserve downstream memoizations.
89+
if (inst.hasValue) {
90+
const currentSelection = inst.value
91+
92+
if (isEqual(currentSelection as Selection, nextSelection)) {
93+
memoizedSelection = currentSelection as Selection
94+
return currentSelection
95+
}
96+
}
97+
}
98+
99+
memoizedSelection = nextSelection
100+
return nextSelection
101+
}
102+
103+
// We may be able to reuse the previous invocation's result.
104+
const prevSnapshot = memoizedSnapshot
105+
const prevSelection = memoizedSelection
106+
107+
const getChangedSegments = (): string[] | void => {
108+
if (
109+
prevSnapshot === undefined ||
110+
!hasAccessed ||
111+
lastUsedProps.length === 0
112+
) {
113+
return undefined
114+
}
115+
116+
const result: string[] = []
117+
118+
if (
119+
nextSnapshot !== null &&
120+
typeof nextSnapshot === 'object' &&
121+
prevSnapshot !== null &&
122+
typeof prevSnapshot === 'object'
123+
) {
124+
for (let i = 0; i < lastUsedProps.length; i++) {
125+
const segmentName = lastUsedProps[i]
126+
127+
if (
128+
(nextSnapshot as Record<string, unknown>)[segmentName] !==
129+
(prevSnapshot as Record<string, unknown>)[segmentName]
130+
) {
131+
result.push(segmentName)
132+
}
133+
}
134+
}
135+
136+
return result
137+
}
138+
139+
if (is(prevSnapshot, nextSnapshot)) {
140+
// The snapshot is the same as last time. Reuse the previous selection.
141+
return prevSelection
142+
}
143+
144+
// The snapshot has changed, so we need to compute a new selection.
145+
const changedSegments = getChangedSegments()
146+
147+
if (changedSegments === undefined || changedSegments.length > 0) {
148+
const nextSelection = selector(getProxy())
149+
lastUsedProps = accessedProps
150+
hasAccessed = true
151+
152+
// If a custom isEqual function is provided, use that to check if the data
153+
// has changed. If it hasn't, return the previous selection. That signals
154+
// to React that the selections are conceptually equal, and we can bail
155+
// out of rendering.
156+
if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
157+
return prevSelection
158+
}
159+
160+
memoizedSnapshot = nextSnapshot
161+
memoizedSelection = nextSelection
162+
return nextSelection
163+
} else {
164+
return prevSelection
165+
}
166+
}
167+
168+
// Assigning this to a constant so that Flow knows it can't change.
169+
const maybeGetServerSnapshot =
170+
getServerSnapshot === undefined ? null : getServerSnapshot
171+
172+
const getSnapshotWithSelector = () => memoizedSelector(getSnapshot())
173+
174+
const getServerSnapshotWithSelector =
175+
maybeGetServerSnapshot === null
176+
? undefined
177+
: () => memoizedSelector(maybeGetServerSnapshot())
178+
return [getSnapshotWithSelector, getServerSnapshotWithSelector]
179+
}, [getSnapshot, getServerSnapshot, selector, isEqual])
180+
const value = useSyncExternalStore(
181+
subscribe,
182+
getSelection,
183+
getServerSelection,
184+
)
185+
useEffect(() => {
186+
// $FlowFixMe[incompatible-type] changing the variant using mutation isn't supported
187+
inst.hasValue = true
188+
// $FlowFixMe[incompatible-type]
189+
inst.value = value
190+
}, [value])
191+
useDebugValue(value)
192+
return value as Selection
193+
}

0 commit comments

Comments
 (0)