main
js 296 lines 8.9 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 type {ReactContext} from 'shared/ReactTypes';
11
12 import {isPrimaryRenderer} from './ReactFizzConfig';
13
14 let rendererSigil;
15 if (__DEV__) {
16 // Use this to detect multiple renderers using the same context
17 rendererSigil = {};
18 }
19
20 // Used to store the parent path of all context overrides in a shared linked list.
21 // Forming a reverse tree.
22 type ContextNode<T> = {
23 parent: null | ContextNode<any>,
24 depth: number, // Short hand to compute the depth of the tree at this node.
25 context: ReactContext<T>,
26 parentValue: T,
27 value: T,
28 };
29
30 // The structure of a context snapshot is an implementation of this file.
31 // Currently, it's implemented as tracking the current active node.
32 export opaque type ContextSnapshot = null | ContextNode<any>;
33
34 export const rootContextSnapshot: ContextSnapshot = null;
35
36 // We assume that this runtime owns the "current" field on all ReactContext instances.
37 // This global (actually thread local) state represents what state all those "current",
38 // fields are currently in.
39 let currentActiveSnapshot: ContextSnapshot = null;
40
41 function popNode(prev: ContextNode<any>): void {
42 // $FlowFixMe[constant-condition]
43 if (isPrimaryRenderer) {
44 prev.context._currentValue = prev.parentValue;
45 } else {
46 prev.context._currentValue2 = prev.parentValue;
47 }
48 }
49
50 function pushNode(next: ContextNode<any>): void {
51 // $FlowFixMe[constant-condition]
52 if (isPrimaryRenderer) {
53 next.context._currentValue = next.value;
54 } else {
55 next.context._currentValue2 = next.value;
56 }
57 }
58
59 function popToNearestCommonAncestor(
60 prev: ContextNode<any>,
61 next: ContextNode<any>,
62 ): void {
63 if (prev === next) {
64 // We've found a shared ancestor. We don't need to pop nor reapply this one or anything above.
65 } else {
66 popNode(prev);
67 const parentPrev = prev.parent;
68 const parentNext = next.parent;
69 if (parentPrev === null) {
70 if (parentNext !== null) {
71 throw new Error(
72 'The stacks must reach the root at the same time. This is a bug in React.',
73 );
74 }
75 } else {
76 if (parentNext === null) {
77 throw new Error(
78 'The stacks must reach the root at the same time. This is a bug in React.',
79 );
80 }
81
82 popToNearestCommonAncestor(parentPrev, parentNext);
83 }
84
85 // On the way back, we push the new ones that weren't common.
86 pushNode(next);
87 }
88 }
89
90 function popAllPrevious(prev: ContextNode<any>): void {
91 popNode(prev);
92 const parentPrev = prev.parent;
93 if (parentPrev !== null) {
94 popAllPrevious(parentPrev);
95 }
96 }
97
98 function pushAllNext(next: ContextNode<any>): void {
99 const parentNext = next.parent;
100 if (parentNext !== null) {
101 pushAllNext(parentNext);
102 }
103 pushNode(next);
104 }
105
106 function popPreviousToCommonLevel(
107 prev: ContextNode<any>,
108 next: ContextNode<any>,
109 ): void {
110 popNode(prev);
111 const parentPrev = prev.parent;
112
113 if (parentPrev === null) {
114 throw new Error(
115 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
116 );
117 }
118
119 if (parentPrev.depth === next.depth) {
120 // We found the same level. Now we just need to find a shared ancestor.
121 popToNearestCommonAncestor(parentPrev, next);
122 } else {
123 // We must still be deeper.
124 popPreviousToCommonLevel(parentPrev, next);
125 }
126 }
127
128 function popNextToCommonLevel(
129 prev: ContextNode<any>,
130 next: ContextNode<any>,
131 ): void {
132 const parentNext = next.parent;
133
134 if (parentNext === null) {
135 throw new Error(
136 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
137 );
138 }
139
140 if (prev.depth === parentNext.depth) {
141 // We found the same level. Now we just need to find a shared ancestor.
142 popToNearestCommonAncestor(prev, parentNext);
143 } else {
144 // We must still be deeper.
145 popNextToCommonLevel(prev, parentNext);
146 }
147 pushNode(next);
148 }
149
150 // Perform context switching to the new snapshot.
151 // To make it cheap to read many contexts, while not suspending, we make the switch eagerly by
152 // updating all the context's current values. That way reads, always just read the current value.
153 // At the cost of updating contexts even if they're never read by this subtree.
154 export function switchContext(newSnapshot: ContextSnapshot): void {
155 // The basic algorithm we need to do is to pop back any contexts that are no longer on the stack.
156 // We also need to update any new contexts that are now on the stack with the deepest value.
157 // The easiest way to update new contexts is to just reapply them in reverse order from the
158 // perspective of the backpointers. To avoid allocating a lot when switching, we use the stack
159 // for that. Therefore this algorithm is recursive.
160 // 1) First we pop which ever snapshot tree was deepest. Popping old contexts as we go.
161 // 2) Then we find the nearest common ancestor from there. Popping old contexts as we go.
162 // 3) Then we reapply new contexts on the way back up the stack.
163 const prev = currentActiveSnapshot;
164 const next = newSnapshot;
165 if (prev !== next) {
166 if (prev === null) {
167 // $FlowFixMe[incompatible-type]: This has to be non-null since it's not equal to prev.
168 pushAllNext(next);
169 } else if (next === null) {
170 popAllPrevious(prev);
171 } else if (prev.depth === next.depth) {
172 popToNearestCommonAncestor(prev, next);
173 } else if (prev.depth > next.depth) {
174 popPreviousToCommonLevel(prev, next);
175 } else {
176 popNextToCommonLevel(prev, next);
177 }
178 currentActiveSnapshot = next;
179 }
180 }
181
182 export function pushProvider<T>(
183 context: ReactContext<T>,
184 nextValue: T,
185 ): ContextSnapshot {
186 let prevValue;
187 // $FlowFixMe[constant-condition]
188 if (isPrimaryRenderer) {
189 prevValue = context._currentValue;
190 context._currentValue = nextValue;
191 if (__DEV__) {
192 if (
193 context._currentRenderer !== undefined &&
194 context._currentRenderer !== null &&
195 context._currentRenderer !== rendererSigil
196 ) {
197 console.error(
198 'Detected multiple renderers concurrently rendering the ' +
199 'same context provider. This is currently unsupported.',
200 );
201 }
202 context._currentRenderer = rendererSigil;
203 }
204 } else {
205 prevValue = context._currentValue2;
206 context._currentValue2 = nextValue;
207 if (__DEV__) {
208 if (
209 context._currentRenderer2 !== undefined &&
210 context._currentRenderer2 !== null &&
211 context._currentRenderer2 !== rendererSigil
212 ) {
213 console.error(
214 'Detected multiple renderers concurrently rendering the ' +
215 'same context provider. This is currently unsupported.',
216 );
217 }
218 context._currentRenderer2 = rendererSigil;
219 }
220 }
221 const prevNode = currentActiveSnapshot;
222 const newNode: ContextNode<T> = {
223 parent: prevNode,
224 depth: prevNode === null ? 0 : prevNode.depth + 1,
225 context: context,
226 parentValue: prevValue,
227 value: nextValue,
228 };
229 currentActiveSnapshot = newNode;
230 return newNode;
231 }
232
233 export function popProvider<T>(context: ReactContext<T>): ContextSnapshot {
234 const prevSnapshot = currentActiveSnapshot;
235
236 if (prevSnapshot === null) {
237 throw new Error(
238 'Tried to pop a Context at the root of the app. This is a bug in React.',
239 );
240 }
241
242 if (__DEV__) {
243 if (prevSnapshot.context !== context) {
244 console.error(
245 'The parent context is not the expected context. This is probably a bug in React.',
246 );
247 }
248 }
249 // $FlowFixMe[constant-condition]
250 if (isPrimaryRenderer) {
251 const value = prevSnapshot.parentValue;
252 prevSnapshot.context._currentValue = value;
253 if (__DEV__) {
254 if (
255 context._currentRenderer !== undefined &&
256 context._currentRenderer !== null &&
257 context._currentRenderer !== rendererSigil
258 ) {
259 console.error(
260 'Detected multiple renderers concurrently rendering the ' +
261 'same context provider. This is currently unsupported.',
262 );
263 }
264 context._currentRenderer = rendererSigil;
265 }
266 } else {
267 const value = prevSnapshot.parentValue;
268 prevSnapshot.context._currentValue2 = value;
269 if (__DEV__) {
270 if (
271 context._currentRenderer2 !== undefined &&
272 context._currentRenderer2 !== null &&
273 context._currentRenderer2 !== rendererSigil
274 ) {
275 console.error(
276 'Detected multiple renderers concurrently rendering the ' +
277 'same context provider. This is currently unsupported.',
278 );
279 }
280 context._currentRenderer2 = rendererSigil;
281 }
282 }
283 return (currentActiveSnapshot = prevSnapshot.parent);
284 }
285
286 export function getActiveContext(): ContextSnapshot {
287 return currentActiveSnapshot;
288 }
289
290 export function readContext<T>(context: ReactContext<T>): T {
291 // $FlowFixMe[constant-condition]
292 const value = isPrimaryRenderer
293 ? context._currentValue
294 : context._currentValue2;
295 return value;
296 }