main
js 627 lines 20.3 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 import type {
12 Fiber,
13 ContextDependency,
14 Dependencies,
15 } from './ReactInternalTypes';
16 import type {StackCursor} from './ReactFiberStack';
17 import type {Lanes} from './ReactFiberLane';
18 import type {TransitionStatus} from './ReactFiberConfig';
19 import type {Hook} from './ReactFiberHooks';
20
21 import {isPrimaryRenderer, HostTransitionContext} from './ReactFiberConfig';
22 import {createCursor, push, pop} from './ReactFiberStack';
23 import {
24 ContextProvider,
25 DehydratedFragment,
26 SuspenseComponent,
27 } from './ReactWorkTags';
28 import {NoLanes, isSubsetOfLanes, mergeLanes} from './ReactFiberLane';
29 import {
30 NoFlags,
31 DidPropagateContext,
32 NeedsPropagation,
33 } from './ReactFiberFlags';
34
35 import is from 'shared/objectIs';
36 import {getHostTransitionProvider} from './ReactFiberHostContext';
37
38 const valueCursor: StackCursor<mixed> = createCursor(null);
39
40 let rendererCursorDEV: StackCursor<Object | null>;
41 if (__DEV__) {
42 rendererCursorDEV = createCursor(null);
43 }
44 let renderer2CursorDEV: StackCursor<Object | null>;
45 if (__DEV__) {
46 renderer2CursorDEV = createCursor(null);
47 }
48
49 let rendererSigil;
50 if (__DEV__) {
51 // Use this to detect multiple renderers using the same context
52 rendererSigil = {};
53 }
54
55 let currentlyRenderingFiber: Fiber | null = null;
56 let lastContextDependency: ContextDependency<mixed> | null = null;
57
58 let isDisallowedContextReadInDEV: boolean = false;
59
60 export function resetContextDependencies(): void {
61 // This is called right before React yields execution, to ensure `readContext`
62 // cannot be called outside the render phase.
63 currentlyRenderingFiber = null;
64 lastContextDependency = null;
65 if (__DEV__) {
66 isDisallowedContextReadInDEV = false;
67 }
68 }
69
70 export function enterDisallowedContextReadInDEV(): void {
71 if (__DEV__) {
72 isDisallowedContextReadInDEV = true;
73 }
74 }
75
76 export function exitDisallowedContextReadInDEV(): void {
77 if (__DEV__) {
78 isDisallowedContextReadInDEV = false;
79 }
80 }
81
82 export function pushProvider<T>(
83 providerFiber: Fiber,
84 context: ReactContext<T>,
85 nextValue: T,
86 ): void {
87 // $FlowFixMe[constant-condition]
88 if (isPrimaryRenderer) {
89 push(valueCursor, context._currentValue, providerFiber);
90
91 context._currentValue = nextValue;
92 if (__DEV__) {
93 push(rendererCursorDEV, context._currentRenderer, providerFiber);
94
95 if (
96 context._currentRenderer !== undefined &&
97 context._currentRenderer !== null &&
98 context._currentRenderer !== rendererSigil
99 ) {
100 console.error(
101 'Detected multiple renderers concurrently rendering the ' +
102 'same context provider. This is currently unsupported.',
103 );
104 }
105 context._currentRenderer = rendererSigil;
106 }
107 } else {
108 push(valueCursor, context._currentValue2, providerFiber);
109
110 context._currentValue2 = nextValue;
111 if (__DEV__) {
112 push(renderer2CursorDEV, context._currentRenderer2, providerFiber);
113
114 if (
115 context._currentRenderer2 !== undefined &&
116 context._currentRenderer2 !== null &&
117 context._currentRenderer2 !== rendererSigil
118 ) {
119 console.error(
120 'Detected multiple renderers concurrently rendering the ' +
121 'same context provider. This is currently unsupported.',
122 );
123 }
124 context._currentRenderer2 = rendererSigil;
125 }
126 }
127 }
128
129 export function popProvider(
130 context: ReactContext<any>,
131 providerFiber: Fiber,
132 ): void {
133 const currentValue = valueCursor.current;
134
135 // $FlowFixMe[constant-condition]
136 if (isPrimaryRenderer) {
137 context._currentValue = currentValue;
138 if (__DEV__) {
139 const currentRenderer = rendererCursorDEV.current;
140 pop(rendererCursorDEV, providerFiber);
141 context._currentRenderer = currentRenderer;
142 }
143 } else {
144 context._currentValue2 = currentValue;
145 if (__DEV__) {
146 const currentRenderer2 = renderer2CursorDEV.current;
147 pop(renderer2CursorDEV, providerFiber);
148 context._currentRenderer2 = currentRenderer2;
149 }
150 }
151
152 pop(valueCursor, providerFiber);
153 }
154
155 export function scheduleContextWorkOnParentPath(
156 parent: Fiber | null,
157 renderLanes: Lanes,
158 propagationRoot: Fiber,
159 ) {
160 // Update the child lanes of all the ancestors, including the alternates.
161 let node = parent;
162 while (node !== null) {
163 const alternate = node.alternate;
164 if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
165 node.childLanes = mergeLanes(node.childLanes, renderLanes);
166 if (alternate !== null) {
167 alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
168 }
169 } else if (
170 alternate !== null &&
171 !isSubsetOfLanes(alternate.childLanes, renderLanes)
172 ) {
173 alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
174 } else {
175 // Neither alternate was updated.
176 // Normally, this would mean that the rest of the
177 // ancestor path already has sufficient priority.
178 // However, this is not necessarily true inside offscreen
179 // or fallback trees because childLanes may be inconsistent
180 // with the surroundings. This is why we continue the loop.
181 }
182 if (node === propagationRoot) {
183 break;
184 }
185 node = node.return;
186 }
187 if (__DEV__) {
188 if (node !== propagationRoot) {
189 console.error(
190 'Expected to find the propagation root when scheduling context work. ' +
191 'This error is likely caused by a bug in React. Please file an issue.',
192 );
193 }
194 }
195 }
196
197 export function propagateContextChange<T>(
198 workInProgress: Fiber,
199 context: ReactContext<T>,
200 renderLanes: Lanes,
201 ): void {
202 // TODO: This path is only used by Cache components. Update
203 // lazilyPropagateParentContextChanges to look for Cache components so they
204 // can take advantage of lazy propagation.
205 const forcePropagateEntireTree = true;
206 propagateContextChanges(
207 workInProgress,
208 [context],
209 renderLanes,
210 forcePropagateEntireTree,
211 );
212 }
213
214 function propagateContextChanges<T>(
215 workInProgress: Fiber,
216 contexts: Array<any>,
217 renderLanes: Lanes,
218 forcePropagateEntireTree: boolean,
219 ): void {
220 let fiber = workInProgress.child;
221 if (fiber !== null) {
222 // Set the return pointer of the child to the work-in-progress fiber.
223 fiber.return = workInProgress;
224 }
225 while (fiber !== null) {
226 let nextFiber;
227
228 // Visit this fiber.
229 const list = fiber.dependencies;
230 if (list !== null) {
231 nextFiber = fiber.child;
232
233 let dep = list.firstContext;
234 findChangedDep: while (dep !== null) {
235 // Assigning these to constants to help Flow
236 const dependency = dep;
237 const consumer = fiber;
238 findContext: for (let i = 0; i < contexts.length; i++) {
239 const context: ReactContext<T> = contexts[i];
240 // Check if the context matches.
241 // $FlowFixMe[invalid-compare]
242 if (dependency.context === context) {
243 // Match! Schedule an update on this fiber.
244
245 // In the lazy implementation, don't mark a dirty flag on the
246 // dependency itself. Not all changes are propagated, so we can't
247 // rely on the propagation function alone to determine whether
248 // something has changed; the consumer will check. In the future, we
249 // could add back a dirty flag as an optimization to avoid double
250 // checking, but until we have selectors it's not really worth
251 // the trouble.
252 consumer.lanes = mergeLanes(consumer.lanes, renderLanes);
253 const alternate = consumer.alternate;
254 if (alternate !== null) {
255 alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
256 }
257 scheduleContextWorkOnParentPath(
258 consumer.return,
259 renderLanes,
260 workInProgress,
261 );
262
263 if (!forcePropagateEntireTree) {
264 // During lazy propagation, when we find a match, we can defer
265 // propagating changes to the children, because we're going to
266 // visit them during render. We should continue propagating the
267 // siblings, though
268 nextFiber = null;
269 }
270
271 // Since we already found a match, we can stop traversing the
272 // dependency list.
273 break findChangedDep;
274 }
275 }
276 dep = dependency.next;
277 }
278 } else if (fiber.tag === DehydratedFragment) {
279 // If a dehydrated suspense boundary is in this subtree, we don't know
280 // if it will have any context consumers in it. The best we can do is
281 // mark it as having updates.
282 const parentSuspense = fiber.return;
283
284 if (parentSuspense === null) {
285 throw new Error(
286 'We just came from a parent so we must have had a parent. This is a bug in React.',
287 );
288 }
289
290 parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
291 const alternate = parentSuspense.alternate;
292 if (alternate !== null) {
293 alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
294 }
295 // This is intentionally passing this fiber as the parent
296 // because we want to schedule this fiber as having work
297 // on its children. We'll use the childLanes on
298 // this fiber to indicate that a context has changed.
299 scheduleContextWorkOnParentPath(
300 parentSuspense,
301 renderLanes,
302 workInProgress,
303 );
304 nextFiber = null;
305 } else if (
306 fiber.tag === SuspenseComponent &&
307 fiber.memoizedState !== null &&
308 fiber.memoizedState.dehydrated === null
309 ) {
310 // This is a client-rendered Suspense boundary that is currently
311 // showing its fallback. The primary children may include context
312 // consumers, but their fibers may not exist in the tree — during
313 // initial mount, if the primary children suspended, their fibers
314 // were discarded since there was no current tree to preserve them.
315 // We can't walk into the primary tree to find consumers, so
316 // conservatively mark the Suspense boundary itself for retry.
317 // When it re-renders, it will re-mount the primary children,
318 // which will read the updated context value.
319 fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
320 const alternate = fiber.alternate;
321 if (alternate !== null) {
322 alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
323 }
324 scheduleContextWorkOnParentPath(
325 fiber.return,
326 renderLanes,
327 workInProgress,
328 );
329 // The primary children's fibers may not exist in the tree (they
330 // were discarded on initial mount if they suspended). However, the
331 // fallback children ARE in the committed tree and visible to the
332 // user. We need to continue propagating into the fallback subtree
333 // so that its context consumers are marked for re-render.
334 //
335 // The fiber structure is:
336 // SuspenseComponent
337 // → child: OffscreenComponent (primary, hidden)
338 // → sibling: FallbackFragment
339 //
340 // Skip the primary (hidden) subtree and jump to the fallback.
341 const primaryChildFragment = fiber.child;
342 if (primaryChildFragment !== null) {
343 nextFiber = primaryChildFragment.sibling;
344 } else {
345 nextFiber = null;
346 }
347 } else {
348 // Traverse down.
349 nextFiber = fiber.child;
350 }
351
352 if (nextFiber !== null) {
353 // Set the return pointer of the child to the work-in-progress fiber.
354 nextFiber.return = fiber;
355 } else {
356 // No child. Traverse to next sibling.
357 nextFiber = fiber;
358 while (nextFiber !== null) {
359 if (nextFiber === workInProgress) {
360 // We're back to the root of this subtree. Exit.
361 nextFiber = null;
362 break;
363 }
364 const sibling = nextFiber.sibling;
365 if (sibling !== null) {
366 // Set the return pointer of the sibling to the work-in-progress fiber.
367 sibling.return = nextFiber.return;
368 nextFiber = sibling;
369 break;
370 }
371 // No more siblings. Traverse up.
372 nextFiber = nextFiber.return;
373 }
374 }
375 fiber = nextFiber;
376 }
377 }
378
379 export function lazilyPropagateParentContextChanges(
380 current: Fiber,
381 workInProgress: Fiber,
382 renderLanes: Lanes,
383 ): boolean {
384 const forcePropagateEntireTree = false;
385 return propagateParentContextChanges(
386 current,
387 workInProgress,
388 renderLanes,
389 forcePropagateEntireTree,
390 );
391 }
392
393 // Used for propagating a deferred tree (Suspense, Offscreen). We must propagate
394 // to the entire subtree, because we won't revisit it until after the current
395 // render has completed, at which point we'll have lost track of which providers
396 // have changed.
397 export function propagateParentContextChangesToDeferredTree(
398 current: Fiber,
399 workInProgress: Fiber,
400 renderLanes: Lanes,
401 ) {
402 const forcePropagateEntireTree = true;
403 propagateParentContextChanges(
404 current,
405 workInProgress,
406 renderLanes,
407 forcePropagateEntireTree,
408 );
409 }
410
411 function propagateParentContextChanges(
412 current: Fiber,
413 workInProgress: Fiber,
414 renderLanes: Lanes,
415 forcePropagateEntireTree: boolean,
416 ): boolean {
417 // Collect all the parent providers that changed. Since this is usually small
418 // number, we use an Array instead of Set.
419 let contexts = null;
420 let parent: null | Fiber = workInProgress;
421 let isInsidePropagationBailout = false;
422 while (parent !== null) {
423 if (!isInsidePropagationBailout) {
424 if ((parent.flags & NeedsPropagation) !== NoFlags) {
425 isInsidePropagationBailout = true;
426 } else if ((parent.flags & DidPropagateContext) !== NoFlags) {
427 break;
428 }
429 }
430
431 if (parent.tag === ContextProvider) {
432 const currentParent = parent.alternate;
433
434 if (currentParent === null) {
435 throw new Error('Should have a current fiber. This is a bug in React.');
436 }
437
438 const oldProps = currentParent.memoizedProps;
439 if (oldProps !== null) {
440 const context: ReactContext<any> = parent.type;
441 const newProps = parent.pendingProps;
442 const newValue = newProps.value;
443
444 const oldValue = oldProps.value;
445
446 if (!is(newValue, oldValue)) {
447 if (contexts !== null) {
448 contexts.push(context);
449 } else {
450 contexts = [context];
451 }
452 }
453 }
454 } else if (parent === getHostTransitionProvider()) {
455 // During a host transition, a host component can act like a context
456 // provider. E.g. in React DOM, this would be a <form />.
457 const currentParent = parent.alternate;
458 if (currentParent === null) {
459 throw new Error('Should have a current fiber. This is a bug in React.');
460 }
461
462 const oldStateHook: Hook = currentParent.memoizedState;
463 const oldState: TransitionStatus = oldStateHook.memoizedState;
464
465 const newStateHook: Hook = parent.memoizedState;
466 const newState: TransitionStatus = newStateHook.memoizedState;
467
468 // This uses regular equality instead of Object.is because we assume that
469 // host transition state doesn't include NaN as a valid type.
470 if (oldState !== newState) {
471 if (contexts !== null) {
472 contexts.push(HostTransitionContext);
473 } else {
474 contexts = [HostTransitionContext];
475 }
476 }
477 }
478 parent = parent.return;
479 }
480
481 if (contexts !== null) {
482 // If there were any changed providers, search through the children and
483 // propagate their changes.
484 propagateContextChanges(
485 workInProgress,
486 contexts,
487 renderLanes,
488 forcePropagateEntireTree,
489 );
490 }
491
492 // This is an optimization so that we only propagate once per subtree. If a
493 // deeply nested child bails out, and it calls this propagation function, it
494 // uses this flag to know that the remaining ancestor providers have already
495 // been propagated.
496 //
497 // NOTE: This optimization is only necessary because we sometimes enter the
498 // begin phase of nodes that don't have any work scheduled on them —
499 // specifically, the siblings of a node that _does_ have scheduled work. The
500 // siblings will bail out and call this function again, even though we already
501 // propagated content changes to it and its subtree. So we use this flag to
502 // mark that the parent providers already propagated.
503 //
504 // Unfortunately, though, we need to ignore this flag when we're inside a
505 // tree whose context propagation was deferred — that's what the
506 // `NeedsPropagation` flag is for.
507 //
508 // If we could instead bail out before entering the siblings' begin phase,
509 // then we could remove both `DidPropagateContext` and `NeedsPropagation`.
510 // Consider this as part of the next refactor to the fiber tree structure.
511 workInProgress.flags |= DidPropagateContext;
512 return contexts !== null;
513 }
514
515 export function checkIfContextChanged(
516 currentDependencies: Dependencies,
517 ): boolean {
518 // Iterate over the current dependencies to see if something changed. This
519 // only gets called if props and state has already bailed out, so it's a
520 // relatively uncommon path, except at the root of a changed subtree.
521 // Alternatively, we could move these comparisons into `readContext`, but
522 // that's a much hotter path, so I think this is an appropriate trade off.
523 let dependency = currentDependencies.firstContext;
524 while (dependency !== null) {
525 const context = dependency.context;
526 // $FlowFixMe[constant-condition]
527 const newValue = isPrimaryRenderer
528 ? context._currentValue
529 : context._currentValue2;
530 const oldValue = dependency.memoizedValue;
531 if (!is(newValue, oldValue)) {
532 return true;
533 }
534 dependency = dependency.next;
535 }
536 return false;
537 }
538
539 export function prepareToReadContext(
540 workInProgress: Fiber,
541 renderLanes: Lanes,
542 ): void {
543 currentlyRenderingFiber = workInProgress;
544 lastContextDependency = null;
545
546 const dependencies = workInProgress.dependencies;
547 if (dependencies !== null) {
548 // Reset the work-in-progress list
549 dependencies.firstContext = null;
550 }
551 }
552
553 export function readContext<T>(context: ReactContext<T>): T {
554 if (__DEV__) {
555 // This warning would fire if you read context inside a Hook like useMemo.
556 // Unlike the class check below, it's not enforced in production for perf.
557 if (isDisallowedContextReadInDEV) {
558 console.error(
559 'Context can only be read while React is rendering. ' +
560 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
561 'In function components, you can read it directly in the function body, but not ' +
562 'inside Hooks like useReducer() or useMemo().',
563 );
564 }
565 }
566 return readContextForConsumer(currentlyRenderingFiber, context);
567 }
568
569 export function readContextDuringReconciliation<T>(
570 consumer: Fiber,
571 context: ReactContext<T>,
572 renderLanes: Lanes,
573 ): T {
574 if (currentlyRenderingFiber === null) {
575 prepareToReadContext(consumer, renderLanes);
576 }
577 return readContextForConsumer(consumer, context);
578 }
579
580 function readContextForConsumer<T>(
581 consumer: Fiber | null,
582 context: ReactContext<T>,
583 ): T {
584 // $FlowFixMe[constant-condition]
585 const value = isPrimaryRenderer
586 ? context._currentValue
587 : context._currentValue2;
588
589 const contextItem = {
590 context: context as any as ReactContext<mixed>,
591 memoizedValue: value,
592 next: null,
593 };
594
595 if (lastContextDependency === null) {
596 if (consumer === null) {
597 throw new Error(
598 'Context can only be read while React is rendering. ' +
599 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
600 'In function components, you can read it directly in the function body, but not ' +
601 'inside Hooks like useReducer() or useMemo().',
602 );
603 }
604
605 // This is the first dependency for this component. Create a new list.
606 // $FlowFixMe[incompatible-type]
607 lastContextDependency = contextItem;
608 consumer.dependencies = __DEV__
609 ? // $FlowFixMe[incompatible-type]
610 {
611 lanes: NoLanes,
612 firstContext: contextItem,
613 _debugThenableState: null,
614 }
615 : // $FlowFixMe[incompatible-type]
616 {
617 lanes: NoLanes,
618 firstContext: contextItem,
619 };
620 consumer.flags |= NeedsPropagation;
621 } else {
622 // Append a new context item.
623 // $FlowFixMe[incompatible-type]
624 lastContextDependency = lastContextDependency.next = contextItem;
625 }
626 return value;
627 }