main
js 989 lines 30.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 {Fiber} from './ReactInternalTypes';
11 import type {
12 Instance,
13 TextInstance,
14 HydratableInstance,
15 ActivityInstance,
16 SuspenseInstance,
17 Container,
18 HostContext,
19 } from './ReactFiberConfig';
20 import type {ActivityState} from './ReactFiberActivityComponent';
21 import type {SuspenseState} from './ReactFiberSuspenseComponent';
22 import type {TreeContext} from './ReactFiberTreeContext';
23 import type {CapturedValue} from './ReactCapturedValue';
24 import type {HydrationDiffNode} from './ReactFiberHydrationDiffs';
25
26 import {
27 HostComponent,
28 HostSingleton,
29 HostRoot,
30 SuspenseComponent,
31 ActivityComponent,
32 } from './ReactWorkTags';
33
34 import {createCapturedValueAtFiber} from './ReactCapturedValue';
35
36 import {createFiberFromDehydratedFragment} from './ReactFiber';
37 import {
38 shouldSetTextContent,
39 supportsHydration,
40 supportsSingletons,
41 getNextHydratableSibling,
42 getNextHydratableSiblingAfterSingleton,
43 getFirstHydratableChild,
44 getFirstHydratableChildWithinContainer,
45 getFirstHydratableChildWithinActivityInstance,
46 getFirstHydratableChildWithinSuspenseInstance,
47 getFirstHydratableChildWithinSingleton,
48 hydrateInstance,
49 diffHydratedPropsForDevWarnings,
50 describeHydratableInstanceForDevWarnings,
51 hydrateTextInstance,
52 diffHydratedTextForDevWarnings,
53 hydrateActivityInstance,
54 hydrateSuspenseInstance,
55 getNextHydratableInstanceAfterActivityInstance,
56 getNextHydratableInstanceAfterSuspenseInstance,
57 shouldDeleteUnhydratedTailInstances,
58 resolveSingletonInstance,
59 canHydrateInstance,
60 canHydrateTextInstance,
61 canHydrateActivityInstance,
62 canHydrateSuspenseInstance,
63 canHydrateFormStateMarker,
64 isFormStateMarkerMatching,
65 validateHydratableInstance,
66 validateHydratableTextInstance,
67 } from './ReactFiberConfig';
68 import {OffscreenLane} from './ReactFiberLane';
69 import {
70 getSuspendedTreeContext,
71 restoreSuspendedTreeContext,
72 } from './ReactFiberTreeContext';
73 import {queueRecoverableErrors} from './ReactFiberWorkLoop';
74 import {getRootHostContainer, getHostContext} from './ReactFiberHostContext';
75 import {describeDiff} from './ReactFiberHydrationDiffs';
76 import {runWithFiberInDEV} from './ReactCurrentFiber';
77
78 // The deepest Fiber on the stack involved in a hydration context.
79 // This may have been an insertion or a hydration.
80 let hydrationParentFiber: null | Fiber = null;
81 let nextHydratableInstance: null | HydratableInstance = null;
82 let isHydrating: boolean = false;
83
84 // This flag allows for warning supression when we expect there to be mismatches
85 // due to earlier mismatches or a suspended fiber.
86 let didSuspendOrErrorDEV: boolean = false;
87
88 // Hydration differences found that haven't yet been logged.
89 let hydrationDiffRootDEV: null | HydrationDiffNode = null;
90
91 // Hydration errors that were thrown inside this boundary
92 let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
93
94 let rootOrSingletonContext = false;
95
96 // Builds a common ancestor tree from the root down for collecting diffs.
97 function buildHydrationDiffNode(
98 fiber: Fiber,
99 distanceFromLeaf: number,
100 ): HydrationDiffNode {
101 if (fiber.return === null) {
102 // We're at the root.
103 if (hydrationDiffRootDEV === null) {
104 hydrationDiffRootDEV = {
105 fiber: fiber,
106 children: [],
107 serverProps: undefined,
108 serverTail: [],
109 distanceFromLeaf: distanceFromLeaf,
110 };
111 } else if (hydrationDiffRootDEV.fiber !== fiber) {
112 throw new Error(
113 'Saw multiple hydration diff roots in a pass. This is a bug in React.',
114 );
115 } else if (hydrationDiffRootDEV.distanceFromLeaf > distanceFromLeaf) {
116 hydrationDiffRootDEV.distanceFromLeaf = distanceFromLeaf;
117 }
118 return hydrationDiffRootDEV;
119 }
120 const siblings = buildHydrationDiffNode(
121 fiber.return,
122 distanceFromLeaf + 1,
123 ).children;
124 // The same node may already exist in the parent. Since we currently always render depth first
125 // and rerender if we suspend or terminate early, if a shared ancestor was added we should still
126 // be inside of that shared ancestor which means it was the last one to be added. If this changes
127 // we may have to scan the whole set.
128 if (siblings.length > 0 && siblings[siblings.length - 1].fiber === fiber) {
129 const existing = siblings[siblings.length - 1];
130 if (existing.distanceFromLeaf > distanceFromLeaf) {
131 existing.distanceFromLeaf = distanceFromLeaf;
132 }
133 return existing;
134 }
135 const newNode: HydrationDiffNode = {
136 fiber: fiber,
137 children: [],
138 serverProps: undefined,
139 serverTail: [],
140 distanceFromLeaf: distanceFromLeaf,
141 };
142 siblings.push(newNode);
143 return newNode;
144 }
145
146 function warnIfHydrating() {
147 if (__DEV__) {
148 if (isHydrating) {
149 console.error(
150 'We should not be hydrating here. This is a bug in React. Please file a bug.',
151 );
152 }
153 }
154 }
155
156 export function markDidThrowWhileHydratingDEV() {
157 if (__DEV__) {
158 didSuspendOrErrorDEV = true;
159 }
160 }
161
162 function enterHydrationState(fiber: Fiber): boolean {
163 // $FlowFixMe[constant-condition]
164 if (!supportsHydration) {
165 return false;
166 }
167
168 const parentInstance: Container = fiber.stateNode.containerInfo;
169 nextHydratableInstance =
170 getFirstHydratableChildWithinContainer(parentInstance);
171 hydrationParentFiber = fiber;
172 isHydrating = true;
173 hydrationErrors = null;
174 didSuspendOrErrorDEV = false;
175 hydrationDiffRootDEV = null;
176 rootOrSingletonContext = true;
177
178 return true;
179 }
180
181 function reenterHydrationStateFromDehydratedActivityInstance(
182 fiber: Fiber,
183 activityInstance: ActivityInstance,
184 treeContext: TreeContext | null,
185 ): boolean {
186 // $FlowFixMe[constant-condition]
187 if (!supportsHydration) {
188 return false;
189 }
190 nextHydratableInstance =
191 getFirstHydratableChildWithinActivityInstance(activityInstance);
192 hydrationParentFiber = fiber;
193 isHydrating = true;
194 hydrationErrors = null;
195 didSuspendOrErrorDEV = false;
196 hydrationDiffRootDEV = null;
197 rootOrSingletonContext = false;
198 if (treeContext !== null) {
199 restoreSuspendedTreeContext(fiber, treeContext);
200 }
201 return true;
202 }
203
204 function reenterHydrationStateFromDehydratedSuspenseInstance(
205 fiber: Fiber,
206 suspenseInstance: SuspenseInstance,
207 treeContext: TreeContext | null,
208 ): boolean {
209 // $FlowFixMe[constant-condition]
210 if (!supportsHydration) {
211 return false;
212 }
213 nextHydratableInstance =
214 getFirstHydratableChildWithinSuspenseInstance(suspenseInstance);
215 hydrationParentFiber = fiber;
216 isHydrating = true;
217 hydrationErrors = null;
218 didSuspendOrErrorDEV = false;
219 hydrationDiffRootDEV = null;
220 rootOrSingletonContext = false;
221 if (treeContext !== null) {
222 restoreSuspendedTreeContext(fiber, treeContext);
223 }
224 return true;
225 }
226
227 function warnNonHydratedInstance(
228 fiber: Fiber,
229 rejectedCandidate: null | HydratableInstance,
230 ) {
231 if (__DEV__) {
232 if (didSuspendOrErrorDEV) {
233 // Inside a boundary that already suspended. We're currently rendering the
234 // siblings of a suspended node. The mismatch may be due to the missing
235 // data, so it's probably a false positive.
236 return;
237 }
238
239 // Add this fiber to the diff tree.
240 const diffNode = buildHydrationDiffNode(fiber, 0);
241 // We use null as a signal that there was no node to match.
242 diffNode.serverProps = null;
243 if (rejectedCandidate !== null) {
244 const description =
245 describeHydratableInstanceForDevWarnings(rejectedCandidate);
246 diffNode.serverTail.push(description);
247 }
248 }
249 }
250
251 function tryHydrateInstance(
252 fiber: Fiber,
253 nextInstance: any,
254 hostContext: HostContext,
255 ) {
256 // fiber is a HostComponent Fiber
257 const instance = canHydrateInstance(
258 nextInstance,
259 fiber.type,
260 fiber.pendingProps,
261 rootOrSingletonContext,
262 );
263 // $FlowFixMe[invalid-compare]
264 // $FlowFixMe[invalid-compare]
265 if (instance !== null) {
266 fiber.stateNode = instance as Instance;
267
268 if (__DEV__) {
269 if (!didSuspendOrErrorDEV) {
270 const differences = diffHydratedPropsForDevWarnings(
271 instance,
272 fiber.type,
273 fiber.pendingProps,
274 hostContext,
275 // $FlowFixMe[invalid-compare]
276 );
277 // $FlowFixMe[invalid-compare]
278 if (differences !== null) {
279 const diffNode = buildHydrationDiffNode(fiber, 0);
280 diffNode.serverProps = differences;
281 }
282 }
283 }
284
285 hydrationParentFiber = fiber;
286 nextHydratableInstance = getFirstHydratableChild(instance);
287 rootOrSingletonContext = false;
288 return true;
289 }
290 return false;
291 }
292
293 function tryHydrateText(fiber: Fiber, nextInstance: any) {
294 // fiber is a HostText Fiber
295 const text = fiber.pendingProps;
296 const textInstance = canHydrateTextInstance(
297 nextInstance,
298 text,
299 // $FlowFixMe[invalid-compare]
300 rootOrSingletonContext,
301 );
302 // $FlowFixMe[invalid-compare]
303 if (textInstance !== null) {
304 fiber.stateNode = textInstance as TextInstance;
305 hydrationParentFiber = fiber;
306 // Text Instances don't have children so there's nothing to hydrate.
307 nextHydratableInstance = null;
308 return true;
309 }
310 return false;
311 }
312
313 function tryHydrateActivity(
314 fiber: Fiber,
315 nextInstance: any,
316 ): null | ActivityInstance {
317 // fiber is a ActivityComponent Fiber
318 const activityInstance = canHydrateActivityInstance(
319 // $FlowFixMe[invalid-compare]
320 nextInstance,
321 rootOrSingletonContext,
322 );
323 // $FlowFixMe[invalid-compare]
324 if (activityInstance !== null) {
325 const activityState: ActivityState = {
326 dehydrated: activityInstance,
327 treeContext: getSuspendedTreeContext(),
328 retryLane: OffscreenLane,
329 hydrationErrors: null,
330 };
331 fiber.memoizedState = activityState;
332 // Store the dehydrated fragment as a child fiber.
333 // This simplifies the code for getHostSibling and deleting nodes,
334 // since it doesn't have to consider all Suspense boundaries and
335 // check if they're dehydrated ones or not.
336 const dehydratedFragment =
337 createFiberFromDehydratedFragment(activityInstance);
338 dehydratedFragment.return = fiber;
339 fiber.child = dehydratedFragment;
340 hydrationParentFiber = fiber;
341 // While an Activity Instance does have children, we won't step into
342 // it during the first pass. Instead, we'll reenter it later.
343 nextHydratableInstance = null;
344 }
345 return activityInstance;
346 }
347
348 function tryHydrateSuspense(
349 fiber: Fiber,
350 nextInstance: any,
351 ): null | SuspenseInstance {
352 // fiber is a SuspenseComponent Fiber
353 // $FlowFixMe[invalid-compare]
354 const suspenseInstance = canHydrateSuspenseInstance(
355 nextInstance,
356 rootOrSingletonContext,
357 );
358 // $FlowFixMe[invalid-compare]
359 if (suspenseInstance !== null) {
360 const suspenseState: SuspenseState = {
361 dehydrated: suspenseInstance,
362 treeContext: getSuspendedTreeContext(),
363 retryLane: OffscreenLane,
364 hydrationErrors: null,
365 };
366 fiber.memoizedState = suspenseState;
367 // Store the dehydrated fragment as a child fiber.
368 // This simplifies the code for getHostSibling and deleting nodes,
369 // since it doesn't have to consider all Suspense boundaries and
370 // check if they're dehydrated ones or not.
371 const dehydratedFragment =
372 createFiberFromDehydratedFragment(suspenseInstance);
373 dehydratedFragment.return = fiber;
374 fiber.child = dehydratedFragment;
375 hydrationParentFiber = fiber;
376 // While a Suspense Instance does have children, we won't step into
377 // it during the first pass. Instead, we'll reenter it later.
378 nextHydratableInstance = null;
379 }
380 return suspenseInstance;
381 }
382
383 export const HydrationMismatchException: mixed = new Error(
384 'Hydration Mismatch Exception: This is not a real error, and should not leak into ' +
385 "userspace. If you're seeing this, it's likely a bug in React.",
386 );
387
388 function throwOnHydrationMismatch(fiber: Fiber, fromText: boolean = false) {
389 let diff = '';
390 if (__DEV__) {
391 // Consume the diff root for this mismatch.
392 // Any other errors will get their own diffs.
393 const diffRoot = hydrationDiffRootDEV;
394 if (diffRoot !== null) {
395 hydrationDiffRootDEV = null;
396 diff = describeDiff(diffRoot);
397 }
398 }
399 const error = new Error(
400 `Hydration failed because the server rendered ${fromText ? 'text' : 'HTML'} didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
401 ` +
402 '\n' +
403 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
404 "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
405 "- Date formatting in a user's locale which doesn't match the server.\n" +
406 '- External changing data without sending a snapshot of it along with the HTML.\n' +
407 '- Invalid HTML tag nesting.\n' +
408 '\n' +
409 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
410 '\n' +
411 'https://react.dev/link/hydration-mismatch' +
412 diff,
413 );
414 queueHydrationError(createCapturedValueAtFiber(error, fiber));
415 throw HydrationMismatchException;
416 }
417
418 function claimHydratableSingleton(fiber: Fiber): void {
419 // $FlowFixMe[constant-condition]
420 if (supportsSingletons) {
421 if (!isHydrating) {
422 return;
423 }
424 const currentRootContainer = getRootHostContainer();
425 const currentHostContext = getHostContext();
426 const instance = (fiber.stateNode = resolveSingletonInstance(
427 fiber.type,
428 fiber.pendingProps,
429 currentRootContainer,
430 currentHostContext,
431 false,
432 ));
433
434 if (__DEV__) {
435 if (!didSuspendOrErrorDEV) {
436 const differences = diffHydratedPropsForDevWarnings(
437 // $FlowFixMe[invalid-compare]
438 instance,
439 fiber.type,
440 fiber.pendingProps,
441 currentHostContext,
442 );
443 // $FlowFixMe[invalid-compare]
444 if (differences !== null) {
445 const diffNode = buildHydrationDiffNode(fiber, 0);
446 diffNode.serverProps = differences;
447 }
448 }
449 }
450
451 hydrationParentFiber = fiber;
452 rootOrSingletonContext = true;
453 nextHydratableInstance = getFirstHydratableChildWithinSingleton(
454 fiber.type,
455 instance,
456 nextHydratableInstance,
457 );
458 }
459 }
460
461 function tryToClaimNextHydratableInstance(fiber: Fiber): void {
462 if (!isHydrating) {
463 return;
464 }
465
466 // Validate that this is ok to render here before any mismatches.
467 const currentHostContext = getHostContext();
468 const shouldKeepWarning = validateHydratableInstance(
469 fiber.type,
470 fiber.pendingProps,
471 currentHostContext,
472 );
473
474 const nextInstance = nextHydratableInstance;
475 if (
476 !nextInstance ||
477 !tryHydrateInstance(fiber, nextInstance, currentHostContext)
478 ) {
479 if (shouldKeepWarning) {
480 warnNonHydratedInstance(fiber, nextInstance);
481 }
482 throwOnHydrationMismatch(fiber);
483 }
484 }
485
486 function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
487 if (!isHydrating) {
488 return;
489 }
490 const text = fiber.pendingProps;
491
492 let shouldKeepWarning = true;
493 // Validate that this is ok to render here before any mismatches.
494 const currentHostContext = getHostContext();
495 shouldKeepWarning = validateHydratableTextInstance(text, currentHostContext);
496
497 const nextInstance = nextHydratableInstance;
498 if (!nextInstance || !tryHydrateText(fiber, nextInstance)) {
499 if (shouldKeepWarning) {
500 warnNonHydratedInstance(fiber, nextInstance);
501 }
502 throwOnHydrationMismatch(fiber);
503 }
504 }
505
506 function claimNextHydratableActivityInstance(fiber: Fiber): ActivityInstance {
507 const nextInstance = nextHydratableInstance;
508 const activityInstance = nextInstance
509 ? tryHydrateActivity(fiber, nextInstance)
510 : null;
511 if (activityInstance === null) {
512 warnNonHydratedInstance(fiber, nextInstance);
513 throw throwOnHydrationMismatch(fiber);
514 }
515 return activityInstance;
516 }
517
518 function claimNextHydratableSuspenseInstance(fiber: Fiber): SuspenseInstance {
519 const nextInstance = nextHydratableInstance;
520 const suspenseInstance = nextInstance
521 ? tryHydrateSuspense(fiber, nextInstance)
522 : null;
523 if (suspenseInstance === null) {
524 warnNonHydratedInstance(fiber, nextInstance);
525 throw throwOnHydrationMismatch(fiber);
526 }
527 return suspenseInstance;
528 }
529
530 export function tryToClaimNextHydratableFormMarkerInstance(
531 fiber: Fiber,
532 ): boolean {
533 if (!isHydrating) {
534 return false;
535 }
536 if (nextHydratableInstance) {
537 const markerInstance = canHydrateFormStateMarker(
538 nextHydratableInstance,
539 rootOrSingletonContext,
540 );
541 if (markerInstance) {
542 // Found the marker instance.
543 nextHydratableInstance = getNextHydratableSibling(markerInstance);
544 // Return true if this marker instance should use the state passed
545 // to hydrateRoot.
546 // TODO: As an optimization, Fizz should only emit these markers if form
547 // state is passed at the root.
548 return isFormStateMarkerMatching(markerInstance);
549 }
550 }
551 // Should have found a marker instance. Throw an error to trigger client
552 // rendering. We don't bother to check if we're in a concurrent root because
553 // useActionState is a new API, so backwards compat is not an issue.
554 throwOnHydrationMismatch(fiber);
555 return false;
556 }
557
558 function prepareToHydrateHostInstance(
559 fiber: Fiber,
560 hostContext: HostContext,
561 ): void {
562 // $FlowFixMe[constant-condition]
563 if (!supportsHydration) {
564 throw new Error(
565 'Expected prepareToHydrateHostInstance() to never be called. ' +
566 'This error is likely caused by a bug in React. Please file an issue.',
567 );
568 }
569
570 const instance: Instance = fiber.stateNode;
571 const didHydrate = hydrateInstance(
572 instance,
573 fiber.type,
574 fiber.memoizedProps,
575 hostContext,
576 fiber,
577 );
578 if (!didHydrate) {
579 throwOnHydrationMismatch(fiber, true);
580 }
581 }
582
583 function prepareToHydrateHostTextInstance(fiber: Fiber): void {
584 // $FlowFixMe[constant-condition]
585 if (!supportsHydration) {
586 throw new Error(
587 'Expected prepareToHydrateHostTextInstance() to never be called. ' +
588 'This error is likely caused by a bug in React. Please file an issue.',
589 );
590 }
591
592 const textInstance: TextInstance = fiber.stateNode;
593 const textContent: string = fiber.memoizedProps;
594 const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
595 let parentProps = null;
596 // We assume that prepareToHydrateHostTextInstance is called in a context where the
597 // hydration parent is the parent host component of this host text.
598 const returnFiber = hydrationParentFiber;
599 if (returnFiber !== null) {
600 switch (returnFiber.tag) {
601 case HostRoot: {
602 if (__DEV__) {
603 // $FlowFixMe[invalid-compare]
604 if (shouldWarnIfMismatchDev) {
605 const difference = diffHydratedTextForDevWarnings(
606 textInstance,
607 textContent,
608 parentProps,
609 );
610 // $FlowFixMe[invalid-compare]
611 if (difference !== null) {
612 const diffNode = buildHydrationDiffNode(fiber, 0);
613 diffNode.serverProps = difference;
614 }
615 }
616 }
617 break;
618 }
619 case HostSingleton:
620 case HostComponent: {
621 parentProps = returnFiber.memoizedProps;
622 // $FlowFixMe[invalid-compare]
623 if (__DEV__) {
624 if (shouldWarnIfMismatchDev) {
625 const difference = diffHydratedTextForDevWarnings(
626 textInstance,
627 textContent,
628 parentProps,
629 );
630 // $FlowFixMe[invalid-compare]
631 if (difference !== null) {
632 const diffNode = buildHydrationDiffNode(fiber, 0);
633 diffNode.serverProps = difference;
634 }
635 }
636 }
637 break;
638 }
639 }
640 // TODO: What if it's a SuspenseInstance?
641 }
642
643 const didHydrate = hydrateTextInstance(
644 textInstance,
645 textContent,
646 fiber,
647 parentProps,
648 );
649 if (!didHydrate) {
650 throwOnHydrationMismatch(fiber, true);
651 }
652 }
653
654 function prepareToHydrateHostActivityInstance(fiber: Fiber): void {
655 // $FlowFixMe[constant-condition]
656 if (!supportsHydration) {
657 throw new Error(
658 'Expected prepareToHydrateHostActivityInstance() to never be called. ' +
659 'This error is likely caused by a bug in React. Please file an issue.',
660 );
661 }
662 const activityState: null | ActivityState = fiber.memoizedState;
663 const activityInstance: null | ActivityInstance =
664 activityState !== null ? activityState.dehydrated : null;
665
666 if (!activityInstance) {
667 throw new Error(
668 'Expected to have a hydrated activity instance. ' +
669 'This error is likely caused by a bug in React. Please file an issue.',
670 );
671 }
672
673 hydrateActivityInstance(activityInstance, fiber);
674 }
675
676 function prepareToHydrateHostSuspenseInstance(fiber: Fiber): void {
677 // $FlowFixMe[constant-condition]
678 if (!supportsHydration) {
679 throw new Error(
680 'Expected prepareToHydrateHostSuspenseInstance() to never be called. ' +
681 'This error is likely caused by a bug in React. Please file an issue.',
682 );
683 }
684
685 const suspenseState: null | SuspenseState = fiber.memoizedState;
686 const suspenseInstance: null | SuspenseInstance =
687 suspenseState !== null ? suspenseState.dehydrated : null;
688
689 if (!suspenseInstance) {
690 throw new Error(
691 'Expected to have a hydrated suspense instance. ' +
692 'This error is likely caused by a bug in React. Please file an issue.',
693 );
694 }
695
696 hydrateSuspenseInstance(suspenseInstance, fiber);
697 }
698
699 function skipPastDehydratedActivityInstance(
700 fiber: Fiber,
701 ): null | HydratableInstance {
702 const activityState: null | ActivityState = fiber.memoizedState;
703 const activityInstance: null | ActivityInstance =
704 activityState !== null ? activityState.dehydrated : null;
705
706 if (!activityInstance) {
707 throw new Error(
708 'Expected to have a hydrated suspense instance. ' +
709 'This error is likely caused by a bug in React. Please file an issue.',
710 );
711 }
712
713 return getNextHydratableInstanceAfterActivityInstance(activityInstance);
714 }
715
716 function skipPastDehydratedSuspenseInstance(
717 fiber: Fiber,
718 ): null | HydratableInstance {
719 // $FlowFixMe[constant-condition]
720 if (!supportsHydration) {
721 throw new Error(
722 'Expected skipPastDehydratedSuspenseInstance() to never be called. ' +
723 'This error is likely caused by a bug in React. Please file an issue.',
724 );
725 }
726 const suspenseState: null | SuspenseState = fiber.memoizedState;
727 const suspenseInstance: null | SuspenseInstance =
728 suspenseState !== null ? suspenseState.dehydrated : null;
729
730 if (!suspenseInstance) {
731 throw new Error(
732 'Expected to have a hydrated suspense instance. ' +
733 'This error is likely caused by a bug in React. Please file an issue.',
734 );
735 }
736
737 return getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance);
738 }
739
740 function popToNextHostParent(fiber: Fiber): void {
741 hydrationParentFiber = fiber.return;
742 while (hydrationParentFiber) {
743 switch (hydrationParentFiber.tag) {
744 case HostComponent:
745 case ActivityComponent:
746 case SuspenseComponent:
747 rootOrSingletonContext = false;
748 return;
749 case HostSingleton:
750 case HostRoot:
751 rootOrSingletonContext = true;
752 return;
753 default:
754 hydrationParentFiber = hydrationParentFiber.return;
755 }
756 }
757 }
758
759 function popHydrationState(fiber: Fiber): boolean {
760 // $FlowFixMe[constant-condition]
761 if (!supportsHydration) {
762 return false;
763 }
764 if (fiber !== hydrationParentFiber) {
765 // We're deeper than the current hydration context, inside an inserted
766 // tree.
767 return false;
768 }
769 if (!isHydrating) {
770 // If we're not currently hydrating but we're in a hydration context, then
771 // we were an insertion and now need to pop up reenter hydration of our
772 // siblings.
773 popToNextHostParent(fiber);
774 isHydrating = true;
775 return false;
776 }
777
778 const tag = fiber.tag;
779
780 // $FlowFixMe[constant-condition]
781 if (supportsSingletons) {
782 // With float we never clear the Root, or Singleton instances. We also do not clear Instances
783 // that have singleton text content
784 if (
785 tag !== HostRoot &&
786 tag !== HostSingleton &&
787 !(
788 tag === HostComponent &&
789 (!shouldDeleteUnhydratedTailInstances(fiber.type) ||
790 shouldSetTextContent(fiber.type, fiber.memoizedProps))
791 )
792 ) {
793 const nextInstance = nextHydratableInstance;
794 if (nextInstance) {
795 warnIfUnhydratedTailNodes(fiber);
796 throwOnHydrationMismatch(fiber);
797 }
798 }
799 } else {
800 // If we have any remaining hydratable nodes, we need to delete them now.
801 // We only do this deeper than head and body since they tend to have random
802 // other nodes in them. We also ignore components with pure text content in
803 // side of them. We also don't delete anything inside the root container.
804 if (
805 tag !== HostRoot &&
806 (tag !== HostComponent ||
807 (shouldDeleteUnhydratedTailInstances(fiber.type) &&
808 !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
809 ) {
810 const nextInstance = nextHydratableInstance;
811 if (nextInstance) {
812 warnIfUnhydratedTailNodes(fiber);
813 throwOnHydrationMismatch(fiber);
814 }
815 }
816 }
817 popToNextHostParent(fiber);
818 if (tag === SuspenseComponent) {
819 nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
820 } else if (tag === ActivityComponent) {
821 nextHydratableInstance = skipPastDehydratedActivityInstance(fiber);
822 // $FlowFixMe[constant-condition]
823 } else if (supportsSingletons && tag === HostSingleton) {
824 nextHydratableInstance = getNextHydratableSiblingAfterSingleton(
825 fiber.type,
826 nextHydratableInstance,
827 );
828 } else {
829 nextHydratableInstance = hydrationParentFiber
830 ? getNextHydratableSibling(fiber.stateNode)
831 : null;
832 }
833 return true;
834 }
835
836 // $FlowFixMe[invalid-compare]
837 function warnIfUnhydratedTailNodes(fiber: Fiber) {
838 if (__DEV__) {
839 let nextInstance = nextHydratableInstance;
840 while (nextInstance) {
841 const diffNode = buildHydrationDiffNode(fiber, 0);
842 const description =
843 describeHydratableInstanceForDevWarnings(nextInstance);
844 diffNode.serverTail.push(description);
845 // $FlowFixMe[invalid-compare]
846 if (description.type === 'Suspense') {
847 const suspenseInstance: SuspenseInstance = nextInstance as any;
848 nextInstance =
849 getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance);
850 } else {
851 nextInstance = getNextHydratableSibling(nextInstance);
852 }
853 }
854 }
855 }
856
857 function resetHydrationState(): void {
858 // $FlowFixMe[constant-condition]
859 if (!supportsHydration) {
860 return;
861 }
862
863 hydrationParentFiber = null;
864 nextHydratableInstance = null;
865 isHydrating = false;
866 didSuspendOrErrorDEV = false;
867 }
868
869 // Restore the hydration cursor when unwinding a HostComponent that already
870 // claimed a DOM node. This is a fork of popHydrationState that does all the
871 // same validity checks but restores the cursor to this fiber's DOM node
872 // instead of advancing past it. It also does NOT clear unhydrated tail nodes
873 // or throw on mismatches since we're unwinding, not completing.
874 //
875 // This is needed when replaySuspendedUnitOfWork calls unwindInterruptedWork
876 // before re-running beginWork on the same fiber, or when throwAndUnwindWorkLoop
877 // calls unwindWork on ancestor fibers.
878 function popHydrationStateOnInterruptedWork(fiber: Fiber): void {
879 // $FlowFixMe[constant-condition]
880 if (!supportsHydration) {
881 return;
882 }
883 if (fiber !== hydrationParentFiber) {
884 // We're deeper than the current hydration context, inside an inserted
885 // tree. Don't touch the cursor.
886 return;
887 }
888 if (!isHydrating) {
889 // If we're not currently hydrating but we're in a hydration context, then
890 // we were an insertion and now need to pop up to reenter hydration of our
891 // siblings. Same as popHydrationState.
892 popToNextHostParent(fiber);
893 isHydrating = true;
894 return;
895 }
896
897 // We're in a valid hydration context. Restore the cursor to this fiber's
898 // DOM node so that when beginWork re-runs, it can claim the same node.
899 // Unlike popHydrationState, we do NOT check for unhydrated tail nodes
900 // or advance the cursor - we're restoring, not completing.
901 popToNextHostParent(fiber);
902 if (fiber.tag === HostComponent && fiber.stateNode != null) {
903 nextHydratableInstance = fiber.stateNode;
904 }
905 }
906
907 export function upgradeHydrationErrorsToRecoverable(): Array<
908 CapturedValue<mixed>,
909 > | null {
910 const queuedErrors = hydrationErrors;
911 if (queuedErrors !== null) {
912 // Successfully completed a forced client render. The errors that occurred
913 // during the hydration attempt are now recovered. We will log them in
914 // commit phase, once the entire tree has finished.
915 queueRecoverableErrors(queuedErrors);
916 hydrationErrors = null;
917 }
918 return queuedErrors;
919 }
920
921 function getIsHydrating(): boolean {
922 return isHydrating;
923 }
924
925 export function queueHydrationError(error: CapturedValue<mixed>): void {
926 if (hydrationErrors === null) {
927 hydrationErrors = [error];
928 } else {
929 hydrationErrors.push(error);
930 }
931 }
932
933 export function emitPendingHydrationWarnings() {
934 if (__DEV__) {
935 // If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
936 // hydrated, however, we might still have DEV-only mismatches that we log now.
937 const diffRoot = hydrationDiffRootDEV;
938 if (diffRoot !== null) {
939 hydrationDiffRootDEV = null;
940 const diff = describeDiff(diffRoot);
941
942 // Just pick the DFS-first leaf as the owner.
943 // Should be good enough since most warnings only have a single error.
944 let diffOwner: HydrationDiffNode = diffRoot;
945 while (diffOwner.children.length > 0) {
946 diffOwner = diffOwner.children[0];
947 }
948
949 runWithFiberInDEV(diffOwner.fiber, () => {
950 console.error(
951 "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
952 'This can happen if a SSR-ed Client Component used:\n' +
953 '\n' +
954 "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
955 "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
956 "- Date formatting in a user's locale which doesn't match the server.\n" +
957 '- External changing data without sending a snapshot of it along with the HTML.\n' +
958 '- Invalid HTML tag nesting.\n' +
959 '\n' +
960 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
961 '\n' +
962 '%s%s',
963 'https://react.dev/link/hydration-mismatch',
964 diff,
965 );
966 });
967 }
968 }
969 }
970
971 export {
972 warnIfHydrating,
973 enterHydrationState,
974 getIsHydrating,
975 reenterHydrationStateFromDehydratedActivityInstance,
976 reenterHydrationStateFromDehydratedSuspenseInstance,
977 resetHydrationState,
978 popHydrationStateOnInterruptedWork,
979 claimHydratableSingleton,
980 tryToClaimNextHydratableInstance,
981 tryToClaimNextHydratableTextInstance,
982 claimNextHydratableActivityInstance,
983 claimNextHydratableSuspenseInstance,
984 prepareToHydrateHostInstance,
985 prepareToHydrateHostTextInstance,
986 prepareToHydrateHostActivityInstance,
987 prepareToHydrateHostSuspenseInstance,
988 popHydrationState,
989 };