|
7 | 7 | * @flow
|
8 | 8 | */
|
9 | 9 |
|
10 |
| -import * as React from 'react'; |
11 |
| -import is from 'shared/objectIs'; |
12 |
| -import invariant from 'shared/invariant'; |
13 | 10 | import {canUseDOM} from 'shared/ExecutionEnvironment';
|
| 11 | +import {useSyncExternalStore as client} from './useSyncExternalStoreClient'; |
| 12 | +import {useSyncExternalStore as server} from './useSyncExternalStoreServer'; |
14 | 13 |
|
15 |
| -// Intentionally not using named imports because Rollup uses dynamic |
16 |
| -// dispatch for CommonJS interop named imports. |
17 |
| -const { |
18 |
| - useState, |
19 |
| - useEffect, |
20 |
| - useLayoutEffect, |
21 |
| - useDebugValue, |
22 |
| - // The built-in API is still prefixed. |
23 |
| - unstable_useSyncExternalStore: builtInAPI, |
24 |
| -} = React; |
25 |
| - |
26 |
| -// TODO: This heuristic doesn't work in React Native. We'll need to provide a |
27 |
| -// special build, using the `.native` extension. |
28 |
| -const isServerEnvironment = !canUseDOM; |
29 |
| - |
30 |
| -// Prefer the built-in API, if it exists. If it doesn't exist, then we assume |
31 |
| -// we're in version 16 or 17, so rendering is always synchronous. The shim |
32 |
| -// does not support concurrent rendering, only the built-in API. |
33 |
| -export const useSyncExternalStore = |
34 |
| - builtInAPI !== undefined |
35 |
| - ? ((builtInAPI: any): typeof useSyncExternalStore_client) |
36 |
| - : isServerEnvironment |
37 |
| - ? useSyncExternalStore_server |
38 |
| - : useSyncExternalStore_client; |
39 |
| - |
40 |
| -let didWarnOld18Alpha = false; |
41 |
| -let didWarnUncachedGetSnapshot = false; |
42 |
| - |
43 |
| -function useSyncExternalStore_server<T>( |
44 |
| - subscribe: (() => void) => () => void, |
45 |
| - getSnapshot: () => T, |
46 |
| - getServerSnapshot?: () => T, |
47 |
| -): T { |
48 |
| - if (getServerSnapshot === undefined) { |
49 |
| - invariant( |
50 |
| - false, |
51 |
| - 'Missing getServerSnapshot, which is required for server-' + |
52 |
| - 'rendered content.', |
53 |
| - ); |
54 |
| - } |
55 |
| - return getServerSnapshot(); |
56 |
| -} |
57 |
| - |
58 |
| -// Disclaimer: This shim breaks many of the rules of React, and only works |
59 |
| -// because of a very particular set of implementation details and assumptions |
60 |
| -// -- change any one of them and it will break. The most important assumption |
61 |
| -// is that updates are always synchronous, because concurrent rendering is |
62 |
| -// only available in versions of React that also have a built-in |
63 |
| -// useSyncExternalStore API. And we only use this shim when the built-in API |
64 |
| -// does not exist. |
65 |
| -// |
66 |
| -// Do not assume that the clever hacks used by this hook also work in general. |
67 |
| -// The point of this shim is to replace the need for hacks by other libraries. |
68 |
| -function useSyncExternalStore_client<T>( |
69 |
| - subscribe: (() => void) => () => void, |
70 |
| - getSnapshot: () => T, |
71 |
| - // Note: The client shim does not use getServerSnapshot, because pre-18 |
72 |
| - // versions of React do not expose a way to check if we're hydrating. So |
73 |
| - // users of the shim will need to track that themselves and return the |
74 |
| - // correct value from `getSnapshot`. |
75 |
| - getServerSnapshot?: () => T, |
76 |
| -): T { |
77 |
| - if (__DEV__) { |
78 |
| - if (!didWarnOld18Alpha) { |
79 |
| - if (React.startTransition !== undefined) { |
80 |
| - didWarnOld18Alpha = true; |
81 |
| - console.error( |
82 |
| - 'You are using an outdated, pre-release alpha of React 18 that ' + |
83 |
| - 'does not support useSyncExternalStore. The ' + |
84 |
| - 'use-sync-external-store shim will not work correctly. Upgrade ' + |
85 |
| - 'to a newer pre-release.', |
86 |
| - ); |
87 |
| - } |
88 |
| - } |
89 |
| - } |
90 |
| - |
91 |
| - // Read the current snapshot from the store on every render. Again, this |
92 |
| - // breaks the rules of React, and only works here because of specific |
93 |
| - // implementation details, most importantly that updates are |
94 |
| - // always synchronous. |
95 |
| - const value = getSnapshot(); |
96 |
| - if (__DEV__) { |
97 |
| - if (!didWarnUncachedGetSnapshot) { |
98 |
| - if (value !== getSnapshot()) { |
99 |
| - console.error( |
100 |
| - 'The result of getSnapshot should be cached to avoid an infinite loop', |
101 |
| - ); |
102 |
| - didWarnUncachedGetSnapshot = true; |
103 |
| - } |
104 |
| - } |
105 |
| - } |
106 |
| - |
107 |
| - // Because updates are synchronous, we don't queue them. Instead we force a |
108 |
| - // re-render whenever the subscribed state changes by updating an some |
109 |
| - // arbitrary useState hook. Then, during render, we call getSnapshot to read |
110 |
| - // the current value. |
111 |
| - // |
112 |
| - // Because we don't actually use the state returned by the useState hook, we |
113 |
| - // can save a bit of memory by storing other stuff in that slot. |
114 |
| - // |
115 |
| - // To implement the early bailout, we need to track some things on a mutable |
116 |
| - // object. Usually, we would put that in a useRef hook, but we can stash it in |
117 |
| - // our useState hook instead. |
118 |
| - // |
119 |
| - // To force a re-render, we call forceUpdate({inst}). That works because the |
120 |
| - // new object always fails an equality check. |
121 |
| - const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}}); |
122 |
| - |
123 |
| - // Track the latest getSnapshot function with a ref. This needs to be updated |
124 |
| - // in the layout phase so we can access it during the tearing check that |
125 |
| - // happens on subscribe. |
126 |
| - useLayoutEffect(() => { |
127 |
| - inst.value = value; |
128 |
| - inst.getSnapshot = getSnapshot; |
129 |
| - |
130 |
| - // Whenever getSnapshot or subscribe changes, we need to check in the |
131 |
| - // commit phase if there was an interleaved mutation. In concurrent mode |
132 |
| - // this can happen all the time, but even in synchronous mode, an earlier |
133 |
| - // effect may have mutated the store. |
134 |
| - if (checkIfSnapshotChanged(inst)) { |
135 |
| - // Force a re-render. |
136 |
| - forceUpdate({inst}); |
137 |
| - } |
138 |
| - }, [subscribe, value, getSnapshot]); |
139 |
| - |
140 |
| - useEffect(() => { |
141 |
| - // Check for changes right before subscribing. Subsequent changes will be |
142 |
| - // detected in the subscription handler. |
143 |
| - if (checkIfSnapshotChanged(inst)) { |
144 |
| - // Force a re-render. |
145 |
| - forceUpdate({inst}); |
146 |
| - } |
147 |
| - const handleStoreChange = () => { |
148 |
| - // TODO: Because there is no cross-renderer API for batching updates, it's |
149 |
| - // up to the consumer of this library to wrap their subscription event |
150 |
| - // with unstable_batchedUpdates. Should we try to detect when this isn't |
151 |
| - // the case and print a warning in development? |
152 |
| - |
153 |
| - // The store changed. Check if the snapshot changed since the last time we |
154 |
| - // read from the store. |
155 |
| - if (checkIfSnapshotChanged(inst)) { |
156 |
| - // Force a re-render. |
157 |
| - forceUpdate({inst}); |
158 |
| - } |
159 |
| - }; |
160 |
| - // Subscribe to the store and return a clean-up function. |
161 |
| - return subscribe(handleStoreChange); |
162 |
| - }, [subscribe]); |
163 |
| - |
164 |
| - useDebugValue(value); |
165 |
| - return value; |
166 |
| -} |
167 |
| - |
168 |
| -function checkIfSnapshotChanged(inst) { |
169 |
| - const latestGetSnapshot = inst.getSnapshot; |
170 |
| - const prevValue = inst.value; |
171 |
| - try { |
172 |
| - const nextValue = latestGetSnapshot(); |
173 |
| - return !is(prevValue, nextValue); |
174 |
| - } catch (error) { |
175 |
| - return true; |
176 |
| - } |
177 |
| -} |
| 14 | +export const useSyncExternalStore = canUseDOM ? client : server; |
0 commit comments