diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index abbbdc1578753..8c282e1bb730e 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -1677,25 +1677,19 @@ describe('ReactDOMFizzServer', () => {
// @gate experimental
it('calls getServerSnapshot instead of getSnapshot', async () => {
- const ref = React.createRef();
-
function getServerSnapshot() {
return 'server';
}
-
function getClientSnapshot() {
return 'client';
}
-
function subscribe() {
return () => {};
}
-
function Child({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
-
function App() {
const value = useSyncExternalStore(
subscribe,
@@ -1703,19 +1697,17 @@ describe('ReactDOMFizzServer', () => {
getServerSnapshot,
);
return (
-
.',
- );
+ Scheduler.unstable_flushAll();
jest.runAllTimers();
// Expect the server-generated HTML to stay intact.
@@ -218,6 +213,101 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('HelloHello');
});
+ it('falls back to client rendering boundary on mismatch', async () => {
+ let client = false;
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => {
+ resolve = () => {
+ suspend = false;
+ resolvePromise();
+ };
+ });
+ function Child() {
+ if (suspend) {
+ Scheduler.unstable_yieldValue('Suspend');
+ throw promise;
+ } else {
+ Scheduler.unstable_yieldValue('Hello');
+ return 'Hello';
+ }
+ }
+ function Component({shouldMismatch}) {
+ Scheduler.unstable_yieldValue('Component');
+ if (shouldMismatch && client) {
+ return
Mismatch;
+ }
+ return
Component
;
+ }
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ const finalHTML = ReactDOMServer.renderToString(
);
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+ expect(Scheduler).toHaveYielded([
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
+
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Component
',
+ );
+
+ suspend = true;
+ client = true;
+
+ ReactDOM.hydrateRoot(container,
);
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
+ jest.runAllTimers();
+
+ // Unchanged
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Component
',
+ );
+
+ suspend = false;
+ resolve();
+ await promise;
+
+ expect(Scheduler).toFlushAndYield([
+ // first pass, mismatches at end
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ // second pass as client render
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
+
+ // Client rendered - suspense comment nodes removed
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Mismatch',
+ );
+ });
+
it('calls the hydration callbacks after hydration or deletion', async () => {
let suspend = false;
let resolve;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index eabc5e43116bb..5e915579a7c0d 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -8,6 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
+import {NoMode, ConcurrentMode} from './ReactTypeOfMode';
import type {
Instance,
TextInstance,
@@ -323,12 +324,21 @@ function tryHydrate(fiber, nextInstance) {
}
}
+function throwOnHydrationMismatchIfConcurrentMode(fiber) {
+ if ((fiber.mode & ConcurrentMode) !== NoMode) {
+ throw new Error(
+ 'An error occurred during hydration. The server HTML was replaced with client content',
+ );
+ }
+}
+
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
// Nothing to hydrate. Make it an insertion.
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
isHydrating = false;
@@ -337,6 +347,7 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
const firstAttemptedInstance = nextInstance;
if (!tryHydrate(fiber, nextInstance)) {
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
// If we can't hydrate this instance let's try the next one.
// We use this as a heuristic. It's based on intuition and not data so it
// might be flawed or unnecessary.
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 48e60581e0f28..e7e08d5f3b8bb 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -8,6 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
+import {NoMode, ConcurrentMode} from './ReactTypeOfMode';
import type {
Instance,
TextInstance,
@@ -323,12 +324,21 @@ function tryHydrate(fiber, nextInstance) {
}
}
+function throwOnHydrationMismatchIfConcurrentMode(fiber) {
+ if ((fiber.mode & ConcurrentMode) !== NoMode) {
+ throw new Error(
+ 'An error occurred during hydration. The server HTML was replaced with client content',
+ );
+ }
+}
+
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
// Nothing to hydrate. Make it an insertion.
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
isHydrating = false;
@@ -337,6 +347,7 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
const firstAttemptedInstance = nextInstance;
if (!tryHydrate(fiber, nextInstance)) {
+ throwOnHydrationMismatchIfConcurrentMode(fiber);
// If we can't hydrate this instance let's try the next one.
// We use this as a heuristic. It's based on intuition and not data so it
// might be flawed or unnecessary.
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 85728d96bb730..cd9931687ba5a 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -268,158 +268,157 @@ function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
}
}
-function markNearestSuspenseBoundaryShouldCapture(
- returnFiber: Fiber,
- sourceFiber: Fiber,
- root: FiberRoot,
- rootRenderLanes: Lanes,
-): Fiber | null {
+function getNearestSuspenseBoundaryToCapture(returnFiber: Fiber) {
+ let node = returnFiber;
const hasInvisibleParentBoundary = hasSuspenseContext(
suspenseStackCursor.current,
(InvisibleParentSuspenseContext: SuspenseContext),
);
- let node = returnFiber;
do {
if (
node.tag === SuspenseComponent &&
shouldCaptureSuspense(node, hasInvisibleParentBoundary)
) {
- // Found the nearest boundary.
- const suspenseBoundary = node;
-
- // This marks a Suspense boundary so that when we're unwinding the stack,
- // it captures the suspended "exception" and does a second (fallback) pass.
-
- if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
- // Legacy Mode Suspense
- //
- // If the boundary is in legacy mode, we should *not*
- // suspend the commit. Pretend as if the suspended component rendered
- // null and keep rendering. When the Suspense boundary completes,
- // we'll do a second pass to render the fallback.
- if (suspenseBoundary === returnFiber) {
- // Special case where we suspended while reconciling the children of
- // a Suspense boundary's inner Offscreen wrapper fiber. This happens
- // when a React.lazy component is a direct child of a
- // Suspense boundary.
- //
- // Suspense boundaries are implemented as multiple fibers, but they
- // are a single conceptual unit. The legacy mode behavior where we
- // pretend the suspended fiber committed as `null` won't work,
- // because in this case the "suspended" fiber is the inner
- // Offscreen wrapper.
- //
- // Because the contents of the boundary haven't started rendering
- // yet (i.e. nothing in the tree has partially rendered) we can
- // switch to the regular, concurrent mode behavior: mark the
- // boundary with ShouldCapture and enter the unwind phase.
- suspenseBoundary.flags |= ShouldCapture;
- } else {
- suspenseBoundary.flags |= DidCapture;
- sourceFiber.flags |= ForceUpdateForLegacySuspense;
+ return node;
+ }
+ // This boundary already captured during this render. Continue to the next
+ // boundary.
+ node = node.return;
+ } while (node !== null);
+ return null;
+}
- // We're going to commit this fiber even though it didn't complete.
- // But we shouldn't call any lifecycle methods or callbacks. Remove
- // all lifecycle effect tags.
- sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
+function markSuspenseBoundaryShouldCapture(
+ suspenseBoundary: Fiber,
+ returnFiber: Fiber,
+ sourceFiber: Fiber,
+ root: FiberRoot,
+ rootRenderLanes: Lanes,
+): Fiber | null {
+ // This marks a Suspense boundary so that when we're unwinding the stack,
+ // it captures the suspended "exception" and does a second (fallback) pass.
+ if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
+ // Legacy Mode Suspense
+ //
+ // If the boundary is in legacy mode, we should *not*
+ // suspend the commit. Pretend as if the suspended component rendered
+ // null and keep rendering. When the Suspense boundary completes,
+ // we'll do a second pass to render the fallback.
+ if (suspenseBoundary === returnFiber) {
+ // Special case where we suspended while reconciling the children of
+ // a Suspense boundary's inner Offscreen wrapper fiber. This happens
+ // when a React.lazy component is a direct child of a
+ // Suspense boundary.
+ //
+ // Suspense boundaries are implemented as multiple fibers, but they
+ // are a single conceptual unit. The legacy mode behavior where we
+ // pretend the suspended fiber committed as `null` won't work,
+ // because in this case the "suspended" fiber is the inner
+ // Offscreen wrapper.
+ //
+ // Because the contents of the boundary haven't started rendering
+ // yet (i.e. nothing in the tree has partially rendered) we can
+ // switch to the regular, concurrent mode behavior: mark the
+ // boundary with ShouldCapture and enter the unwind phase.
+ suspenseBoundary.flags |= ShouldCapture;
+ } else {
+ suspenseBoundary.flags |= DidCapture;
+ sourceFiber.flags |= ForceUpdateForLegacySuspense;
- if (supportsPersistence && enablePersistentOffscreenHostContainer) {
- // Another legacy Suspense quirk. In persistent mode, if this is the
- // initial mount, override the props of the host container to hide
- // its contents.
- const currentSuspenseBoundary = suspenseBoundary.alternate;
- if (currentSuspenseBoundary === null) {
- const offscreenFiber: Fiber = (suspenseBoundary.child: any);
- const offscreenContainer = offscreenFiber.child;
- if (offscreenContainer !== null) {
- const children = offscreenContainer.memoizedProps.children;
- const containerProps = getOffscreenContainerProps(
- 'hidden',
- children,
- );
- offscreenContainer.pendingProps = containerProps;
- offscreenContainer.memoizedProps = containerProps;
- }
- }
- }
+ // We're going to commit this fiber even though it didn't complete.
+ // But we shouldn't call any lifecycle methods or callbacks. Remove
+ // all lifecycle effect tags.
+ sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
- if (sourceFiber.tag === ClassComponent) {
- const currentSourceFiber = sourceFiber.alternate;
- if (currentSourceFiber === null) {
- // This is a new mount. Change the tag so it's not mistaken for a
- // completed class component. For example, we should not call
- // componentWillUnmount if it is deleted.
- sourceFiber.tag = IncompleteClassComponent;
- } else {
- // When we try rendering again, we should not reuse the current fiber,
- // since it's known to be in an inconsistent state. Use a force update to
- // prevent a bail out.
- const update = createUpdate(NoTimestamp, SyncLane);
- update.tag = ForceUpdate;
- enqueueUpdate(sourceFiber, update, SyncLane);
- }
+ if (supportsPersistence && enablePersistentOffscreenHostContainer) {
+ // Another legacy Suspense quirk. In persistent mode, if this is the
+ // initial mount, override the props of the host container to hide
+ // its contents.
+ const currentSuspenseBoundary = suspenseBoundary.alternate;
+ if (currentSuspenseBoundary === null) {
+ const offscreenFiber: Fiber = (suspenseBoundary.child: any);
+ const offscreenContainer = offscreenFiber.child;
+ if (offscreenContainer !== null) {
+ const children = offscreenContainer.memoizedProps.children;
+ const containerProps = getOffscreenContainerProps(
+ 'hidden',
+ children,
+ );
+ offscreenContainer.pendingProps = containerProps;
+ offscreenContainer.memoizedProps = containerProps;
}
+ }
+ }
- // The source fiber did not complete. Mark it with Sync priority to
- // indicate that it still has pending work.
- sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
+ if (sourceFiber.tag === ClassComponent) {
+ const currentSourceFiber = sourceFiber.alternate;
+ if (currentSourceFiber === null) {
+ // This is a new mount. Change the tag so it's not mistaken for a
+ // completed class component. For example, we should not call
+ // componentWillUnmount if it is deleted.
+ sourceFiber.tag = IncompleteClassComponent;
+ } else {
+ // When we try rendering again, we should not reuse the current fiber,
+ // since it's known to be in an inconsistent state. Use a force update to
+ // prevent a bail out.
+ const update = createUpdate(NoTimestamp, SyncLane);
+ update.tag = ForceUpdate;
+ enqueueUpdate(sourceFiber, update, SyncLane);
}
- return suspenseBoundary;
}
- // Confirmed that the boundary is in a concurrent mode tree. Continue
- // with the normal suspend path.
- //
- // After this we'll use a set of heuristics to determine whether this
- // render pass will run to completion or restart or "suspend" the commit.
- // The actual logic for this is spread out in different places.
- //
- // This first principle is that if we're going to suspend when we complete
- // a root, then we should also restart if we get an update or ping that
- // might unsuspend it, and vice versa. The only reason to suspend is
- // because you think you might want to restart before committing. However,
- // it doesn't make sense to restart only while in the period we're suspended.
- //
- // Restarting too aggressively is also not good because it starves out any
- // intermediate loading state. So we use heuristics to determine when.
- // Suspense Heuristics
- //
- // If nothing threw a Promise or all the same fallbacks are already showing,
- // then don't suspend/restart.
- //
- // If this is an initial render of a new tree of Suspense boundaries and
- // those trigger a fallback, then don't suspend/restart. We want to ensure
- // that we can show the initial loading state as quickly as possible.
- //
- // If we hit a "Delayed" case, such as when we'd switch from content back into
- // a fallback, then we should always suspend/restart. Transitions apply
- // to this case. If none is defined, JND is used instead.
- //
- // If we're already showing a fallback and it gets "retried", allowing us to show
- // another level, but there's still an inner boundary that would show a fallback,
- // then we suspend/restart for 500ms since the last time we showed a fallback
- // anywhere in the tree. This effectively throttles progressive loading into a
- // consistent train of commits. This also gives us an opportunity to restart to
- // get to the completed state slightly earlier.
- //
- // If there's ambiguity due to batching it's resolved in preference of:
- // 1) "delayed", 2) "initial render", 3) "retry".
- //
- // We want to ensure that a "busy" state doesn't get force committed. We want to
- // ensure that new initial loading states can commit as soon as possible.
- suspenseBoundary.flags |= ShouldCapture;
- // TODO: I think we can remove this, since we now use `DidCapture` in
- // the begin phase to prevent an early bailout.
- suspenseBoundary.lanes = rootRenderLanes;
- return suspenseBoundary;
+ // The source fiber did not complete. Mark it with Sync priority to
+ // indicate that it still has pending work.
+ sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
}
- // This boundary already captured during this render. Continue to the next
- // boundary.
- node = node.return;
- } while (node !== null);
+ return suspenseBoundary;
+ }
+ // Confirmed that the boundary is in a concurrent mode tree. Continue
+ // with the normal suspend path.
+ //
+ // After this we'll use a set of heuristics to determine whether this
+ // render pass will run to completion or restart or "suspend" the commit.
+ // The actual logic for this is spread out in different places.
+ //
+ // This first principle is that if we're going to suspend when we complete
+ // a root, then we should also restart if we get an update or ping that
+ // might unsuspend it, and vice versa. The only reason to suspend is
+ // because you think you might want to restart before committing. However,
+ // it doesn't make sense to restart only while in the period we're suspended.
+ //
+ // Restarting too aggressively is also not good because it starves out any
+ // intermediate loading state. So we use heuristics to determine when.
- // Could not find a Suspense boundary capable of capturing.
- return null;
+ // Suspense Heuristics
+ //
+ // If nothing threw a Promise or all the same fallbacks are already showing,
+ // then don't suspend/restart.
+ //
+ // If this is an initial render of a new tree of Suspense boundaries and
+ // those trigger a fallback, then don't suspend/restart. We want to ensure
+ // that we can show the initial loading state as quickly as possible.
+ //
+ // If we hit a "Delayed" case, such as when we'd switch from content back into
+ // a fallback, then we should always suspend/restart. Transitions apply
+ // to this case. If none is defined, JND is used instead.
+ //
+ // If we're already showing a fallback and it gets "retried", allowing us to show
+ // another level, but there's still an inner boundary that would show a fallback,
+ // then we suspend/restart for 500ms since the last time we showed a fallback
+ // anywhere in the tree. This effectively throttles progressive loading into a
+ // consistent train of commits. This also gives us an opportunity to restart to
+ // get to the completed state slightly earlier.
+ //
+ // If there's ambiguity due to batching it's resolved in preference of:
+ // 1) "delayed", 2) "initial render", 3) "retry".
+ //
+ // We want to ensure that a "busy" state doesn't get force committed. We want to
+ // ensure that new initial loading states can commit as soon as possible.
+ suspenseBoundary.flags |= ShouldCapture;
+ // TODO: I think we can remove this, since we now use `DidCapture` in
+ // the begin phase to prevent an early bailout.
+ suspenseBoundary.lanes = rootRenderLanes;
+ return suspenseBoundary;
}
function throwException(
@@ -458,13 +457,16 @@ function throwException(
}
// Schedule the nearest Suspense to re-render the timed out view.
- const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
- returnFiber,
- sourceFiber,
- root,
- rootRenderLanes,
- );
+ const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
if (suspenseBoundary !== null) {
+ suspenseBoundary.flags &= ~ForceClientRender;
+ markSuspenseBoundaryShouldCapture(
+ suspenseBoundary,
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
attachWakeableListeners(
suspenseBoundary,
root,
@@ -487,20 +489,24 @@ function throwException(
} else {
// This is a regular error, not a Suspense wakeable.
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
+ const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
// If the error was thrown during hydration, we may be able to recover by
// discarding the dehydrated content and switching to a client render.
// Instead of surfacing the error, find the nearest Suspense boundary
// and render it again without hydration.
- const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
- returnFiber,
- sourceFiber,
- root,
- rootRenderLanes,
- );
if (suspenseBoundary !== null) {
- // Set a flag to indicate that we should try rendering the normal
- // children again, not the fallback.
- suspenseBoundary.flags |= ForceClientRender;
+ if ((suspenseBoundary.flags & ShouldCapture) === NoFlags) {
+ // Set a flag to indicate that we should try rendering the normal
+ // children again, not the fallback.
+ suspenseBoundary.flags |= ForceClientRender;
+ }
+ markSuspenseBoundaryShouldCapture(
+ suspenseBoundary,
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
return;
}
} else {
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index 27b0719ba8532..8f6d18a48dea3 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -268,158 +268,157 @@ function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
}
}
-function markNearestSuspenseBoundaryShouldCapture(
- returnFiber: Fiber,
- sourceFiber: Fiber,
- root: FiberRoot,
- rootRenderLanes: Lanes,
-): Fiber | null {
+function getNearestSuspenseBoundaryToCapture(returnFiber: Fiber) {
+ let node = returnFiber;
const hasInvisibleParentBoundary = hasSuspenseContext(
suspenseStackCursor.current,
(InvisibleParentSuspenseContext: SuspenseContext),
);
- let node = returnFiber;
do {
if (
node.tag === SuspenseComponent &&
shouldCaptureSuspense(node, hasInvisibleParentBoundary)
) {
- // Found the nearest boundary.
- const suspenseBoundary = node;
-
- // This marks a Suspense boundary so that when we're unwinding the stack,
- // it captures the suspended "exception" and does a second (fallback) pass.
-
- if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
- // Legacy Mode Suspense
- //
- // If the boundary is in legacy mode, we should *not*
- // suspend the commit. Pretend as if the suspended component rendered
- // null and keep rendering. When the Suspense boundary completes,
- // we'll do a second pass to render the fallback.
- if (suspenseBoundary === returnFiber) {
- // Special case where we suspended while reconciling the children of
- // a Suspense boundary's inner Offscreen wrapper fiber. This happens
- // when a React.lazy component is a direct child of a
- // Suspense boundary.
- //
- // Suspense boundaries are implemented as multiple fibers, but they
- // are a single conceptual unit. The legacy mode behavior where we
- // pretend the suspended fiber committed as `null` won't work,
- // because in this case the "suspended" fiber is the inner
- // Offscreen wrapper.
- //
- // Because the contents of the boundary haven't started rendering
- // yet (i.e. nothing in the tree has partially rendered) we can
- // switch to the regular, concurrent mode behavior: mark the
- // boundary with ShouldCapture and enter the unwind phase.
- suspenseBoundary.flags |= ShouldCapture;
- } else {
- suspenseBoundary.flags |= DidCapture;
- sourceFiber.flags |= ForceUpdateForLegacySuspense;
+ return node;
+ }
+ // This boundary already captured during this render. Continue to the next
+ // boundary.
+ node = node.return;
+ } while (node !== null);
+ return null;
+}
- // We're going to commit this fiber even though it didn't complete.
- // But we shouldn't call any lifecycle methods or callbacks. Remove
- // all lifecycle effect tags.
- sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
+function markSuspenseBoundaryShouldCapture(
+ suspenseBoundary: Fiber,
+ returnFiber: Fiber,
+ sourceFiber: Fiber,
+ root: FiberRoot,
+ rootRenderLanes: Lanes,
+): Fiber | null {
+ // This marks a Suspense boundary so that when we're unwinding the stack,
+ // it captures the suspended "exception" and does a second (fallback) pass.
+ if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
+ // Legacy Mode Suspense
+ //
+ // If the boundary is in legacy mode, we should *not*
+ // suspend the commit. Pretend as if the suspended component rendered
+ // null and keep rendering. When the Suspense boundary completes,
+ // we'll do a second pass to render the fallback.
+ if (suspenseBoundary === returnFiber) {
+ // Special case where we suspended while reconciling the children of
+ // a Suspense boundary's inner Offscreen wrapper fiber. This happens
+ // when a React.lazy component is a direct child of a
+ // Suspense boundary.
+ //
+ // Suspense boundaries are implemented as multiple fibers, but they
+ // are a single conceptual unit. The legacy mode behavior where we
+ // pretend the suspended fiber committed as `null` won't work,
+ // because in this case the "suspended" fiber is the inner
+ // Offscreen wrapper.
+ //
+ // Because the contents of the boundary haven't started rendering
+ // yet (i.e. nothing in the tree has partially rendered) we can
+ // switch to the regular, concurrent mode behavior: mark the
+ // boundary with ShouldCapture and enter the unwind phase.
+ suspenseBoundary.flags |= ShouldCapture;
+ } else {
+ suspenseBoundary.flags |= DidCapture;
+ sourceFiber.flags |= ForceUpdateForLegacySuspense;
- if (supportsPersistence && enablePersistentOffscreenHostContainer) {
- // Another legacy Suspense quirk. In persistent mode, if this is the
- // initial mount, override the props of the host container to hide
- // its contents.
- const currentSuspenseBoundary = suspenseBoundary.alternate;
- if (currentSuspenseBoundary === null) {
- const offscreenFiber: Fiber = (suspenseBoundary.child: any);
- const offscreenContainer = offscreenFiber.child;
- if (offscreenContainer !== null) {
- const children = offscreenContainer.memoizedProps.children;
- const containerProps = getOffscreenContainerProps(
- 'hidden',
- children,
- );
- offscreenContainer.pendingProps = containerProps;
- offscreenContainer.memoizedProps = containerProps;
- }
- }
- }
+ // We're going to commit this fiber even though it didn't complete.
+ // But we shouldn't call any lifecycle methods or callbacks. Remove
+ // all lifecycle effect tags.
+ sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
- if (sourceFiber.tag === ClassComponent) {
- const currentSourceFiber = sourceFiber.alternate;
- if (currentSourceFiber === null) {
- // This is a new mount. Change the tag so it's not mistaken for a
- // completed class component. For example, we should not call
- // componentWillUnmount if it is deleted.
- sourceFiber.tag = IncompleteClassComponent;
- } else {
- // When we try rendering again, we should not reuse the current fiber,
- // since it's known to be in an inconsistent state. Use a force update to
- // prevent a bail out.
- const update = createUpdate(NoTimestamp, SyncLane);
- update.tag = ForceUpdate;
- enqueueUpdate(sourceFiber, update, SyncLane);
- }
+ if (supportsPersistence && enablePersistentOffscreenHostContainer) {
+ // Another legacy Suspense quirk. In persistent mode, if this is the
+ // initial mount, override the props of the host container to hide
+ // its contents.
+ const currentSuspenseBoundary = suspenseBoundary.alternate;
+ if (currentSuspenseBoundary === null) {
+ const offscreenFiber: Fiber = (suspenseBoundary.child: any);
+ const offscreenContainer = offscreenFiber.child;
+ if (offscreenContainer !== null) {
+ const children = offscreenContainer.memoizedProps.children;
+ const containerProps = getOffscreenContainerProps(
+ 'hidden',
+ children,
+ );
+ offscreenContainer.pendingProps = containerProps;
+ offscreenContainer.memoizedProps = containerProps;
}
+ }
+ }
- // The source fiber did not complete. Mark it with Sync priority to
- // indicate that it still has pending work.
- sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
+ if (sourceFiber.tag === ClassComponent) {
+ const currentSourceFiber = sourceFiber.alternate;
+ if (currentSourceFiber === null) {
+ // This is a new mount. Change the tag so it's not mistaken for a
+ // completed class component. For example, we should not call
+ // componentWillUnmount if it is deleted.
+ sourceFiber.tag = IncompleteClassComponent;
+ } else {
+ // When we try rendering again, we should not reuse the current fiber,
+ // since it's known to be in an inconsistent state. Use a force update to
+ // prevent a bail out.
+ const update = createUpdate(NoTimestamp, SyncLane);
+ update.tag = ForceUpdate;
+ enqueueUpdate(sourceFiber, update, SyncLane);
}
- return suspenseBoundary;
}
- // Confirmed that the boundary is in a concurrent mode tree. Continue
- // with the normal suspend path.
- //
- // After this we'll use a set of heuristics to determine whether this
- // render pass will run to completion or restart or "suspend" the commit.
- // The actual logic for this is spread out in different places.
- //
- // This first principle is that if we're going to suspend when we complete
- // a root, then we should also restart if we get an update or ping that
- // might unsuspend it, and vice versa. The only reason to suspend is
- // because you think you might want to restart before committing. However,
- // it doesn't make sense to restart only while in the period we're suspended.
- //
- // Restarting too aggressively is also not good because it starves out any
- // intermediate loading state. So we use heuristics to determine when.
- // Suspense Heuristics
- //
- // If nothing threw a Promise or all the same fallbacks are already showing,
- // then don't suspend/restart.
- //
- // If this is an initial render of a new tree of Suspense boundaries and
- // those trigger a fallback, then don't suspend/restart. We want to ensure
- // that we can show the initial loading state as quickly as possible.
- //
- // If we hit a "Delayed" case, such as when we'd switch from content back into
- // a fallback, then we should always suspend/restart. Transitions apply
- // to this case. If none is defined, JND is used instead.
- //
- // If we're already showing a fallback and it gets "retried", allowing us to show
- // another level, but there's still an inner boundary that would show a fallback,
- // then we suspend/restart for 500ms since the last time we showed a fallback
- // anywhere in the tree. This effectively throttles progressive loading into a
- // consistent train of commits. This also gives us an opportunity to restart to
- // get to the completed state slightly earlier.
- //
- // If there's ambiguity due to batching it's resolved in preference of:
- // 1) "delayed", 2) "initial render", 3) "retry".
- //
- // We want to ensure that a "busy" state doesn't get force committed. We want to
- // ensure that new initial loading states can commit as soon as possible.
- suspenseBoundary.flags |= ShouldCapture;
- // TODO: I think we can remove this, since we now use `DidCapture` in
- // the begin phase to prevent an early bailout.
- suspenseBoundary.lanes = rootRenderLanes;
- return suspenseBoundary;
+ // The source fiber did not complete. Mark it with Sync priority to
+ // indicate that it still has pending work.
+ sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
}
- // This boundary already captured during this render. Continue to the next
- // boundary.
- node = node.return;
- } while (node !== null);
+ return suspenseBoundary;
+ }
+ // Confirmed that the boundary is in a concurrent mode tree. Continue
+ // with the normal suspend path.
+ //
+ // After this we'll use a set of heuristics to determine whether this
+ // render pass will run to completion or restart or "suspend" the commit.
+ // The actual logic for this is spread out in different places.
+ //
+ // This first principle is that if we're going to suspend when we complete
+ // a root, then we should also restart if we get an update or ping that
+ // might unsuspend it, and vice versa. The only reason to suspend is
+ // because you think you might want to restart before committing. However,
+ // it doesn't make sense to restart only while in the period we're suspended.
+ //
+ // Restarting too aggressively is also not good because it starves out any
+ // intermediate loading state. So we use heuristics to determine when.
- // Could not find a Suspense boundary capable of capturing.
- return null;
+ // Suspense Heuristics
+ //
+ // If nothing threw a Promise or all the same fallbacks are already showing,
+ // then don't suspend/restart.
+ //
+ // If this is an initial render of a new tree of Suspense boundaries and
+ // those trigger a fallback, then don't suspend/restart. We want to ensure
+ // that we can show the initial loading state as quickly as possible.
+ //
+ // If we hit a "Delayed" case, such as when we'd switch from content back into
+ // a fallback, then we should always suspend/restart. Transitions apply
+ // to this case. If none is defined, JND is used instead.
+ //
+ // If we're already showing a fallback and it gets "retried", allowing us to show
+ // another level, but there's still an inner boundary that would show a fallback,
+ // then we suspend/restart for 500ms since the last time we showed a fallback
+ // anywhere in the tree. This effectively throttles progressive loading into a
+ // consistent train of commits. This also gives us an opportunity to restart to
+ // get to the completed state slightly earlier.
+ //
+ // If there's ambiguity due to batching it's resolved in preference of:
+ // 1) "delayed", 2) "initial render", 3) "retry".
+ //
+ // We want to ensure that a "busy" state doesn't get force committed. We want to
+ // ensure that new initial loading states can commit as soon as possible.
+ suspenseBoundary.flags |= ShouldCapture;
+ // TODO: I think we can remove this, since we now use `DidCapture` in
+ // the begin phase to prevent an early bailout.
+ suspenseBoundary.lanes = rootRenderLanes;
+ return suspenseBoundary;
}
function throwException(
@@ -458,13 +457,16 @@ function throwException(
}
// Schedule the nearest Suspense to re-render the timed out view.
- const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
- returnFiber,
- sourceFiber,
- root,
- rootRenderLanes,
- );
+ const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
if (suspenseBoundary !== null) {
+ suspenseBoundary.flags &= ~ForceClientRender;
+ markSuspenseBoundaryShouldCapture(
+ suspenseBoundary,
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
attachWakeableListeners(
suspenseBoundary,
root,
@@ -487,20 +489,24 @@ function throwException(
} else {
// This is a regular error, not a Suspense wakeable.
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
+ const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
// If the error was thrown during hydration, we may be able to recover by
// discarding the dehydrated content and switching to a client render.
// Instead of surfacing the error, find the nearest Suspense boundary
// and render it again without hydration.
- const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
- returnFiber,
- sourceFiber,
- root,
- rootRenderLanes,
- );
if (suspenseBoundary !== null) {
- // Set a flag to indicate that we should try rendering the normal
- // children again, not the fallback.
- suspenseBoundary.flags |= ForceClientRender;
+ if ((suspenseBoundary.flags & ShouldCapture) === NoFlags) {
+ // Set a flag to indicate that we should try rendering the normal
+ // children again, not the fallback.
+ suspenseBoundary.flags |= ForceClientRender;
+ }
+ markSuspenseBoundaryShouldCapture(
+ suspenseBoundary,
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
return;
}
} else {
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 93391eb309d49..6703e2cd36539 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -402,5 +402,6 @@
"414": "Did not expect this call in production. This is a bug in React. Please file an issue.",
"415": "Error parsing the data. It's probably an error code or network corruption.",
"416": "This environment don't support binary chunks.",
- "417": "React currently only supports piping to one writable stream."
+ "417": "React currently only supports piping to one writable stream.",
+ "418": "An error occurred during hydration. The server HTML was replaced with client content"
}