|
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