diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index edf5814023142..dd43cbd20d279 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -1783,4 +1783,263 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(
client
);
expect(ref.current).toEqual(serverRenderedDiv);
});
+
+ // @gate supportsNativeUseSyncExternalStore
+ // @gate experimental
+ it(
+ 'errors during hydration force a client render at the nearest Suspense ' +
+ 'boundary, and during the client render it recovers',
+ async () => {
+ let isClient = false;
+
+ function subscribe() {
+ return () => {};
+ }
+ function getClientSnapshot() {
+ return 'Yay!';
+ }
+
+ // At the time of writing, the only API that exposes whether it's currently
+ // hydrating is the `getServerSnapshot` API, so I'm using that here to
+ // simulate an error during hydration.
+ function getServerSnapshot() {
+ if (isClient) {
+ throw new Error('Hydration error');
+ }
+ return 'Yay!';
+ }
+
+ function Child() {
+ const value = useSyncExternalStore(
+ subscribe,
+ getClientSnapshot,
+ getServerSnapshot,
+ );
+ Scheduler.unstable_yieldValue(value);
+ return value;
+ }
+
+ const span1Ref = React.createRef();
+ const span2Ref = React.createRef();
+ const span3Ref = React.createRef();
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+ expect(Scheduler).toHaveYielded(['Yay!']);
+
+ const [span1, span2, span3] = container.getElementsByTagName('span');
+
+ // Hydrate the tree. Child will throw during hydration, but not when it
+ // falls back to client rendering.
+ isClient = true;
+ ReactDOM.hydrateRoot(container, );
+
+ expect(Scheduler).toFlushAndYield(['Yay!']);
+ expect(getVisibleChildren(container)).toEqual(
+
+
+ Yay!
+
+
,
+ );
+
+ // The node that's inside the boundary that errored during hydration was
+ // not hydrated.
+ expect(span2Ref.current).not.toBe(span2);
+
+ // But the nodes outside the boundary were.
+ expect(span1Ref.current).toBe(span1);
+ expect(span3Ref.current).toBe(span3);
+ },
+ );
+
+ // @gate experimental
+ it(
+ 'errors during hydration force a client render at the nearest Suspense ' +
+ 'boundary, and during the client render it fails again',
+ async () => {
+ // Similar to previous test, but the client render errors, too. We should
+ // be able to capture it with an error boundary.
+
+ let isClient = false;
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ if (this.state.error !== null) {
+ return this.state.error.message;
+ }
+ return this.props.children;
+ }
+ }
+
+ function Child() {
+ if (isClient) {
+ throw new Error('Oops!');
+ }
+ Scheduler.unstable_yieldValue('Yay!');
+ return 'Yay!';
+ }
+
+ const span1Ref = React.createRef();
+ const span2Ref = React.createRef();
+ const span3Ref = React.createRef();
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+ expect(Scheduler).toHaveYielded(['Yay!']);
+
+ // Hydrate the tree. Child will throw during render.
+ isClient = true;
+ ReactDOM.hydrateRoot(container, );
+
+ expect(Scheduler).toFlushAndYield([]);
+ expect(getVisibleChildren(container)).toEqual('Oops!');
+ },
+ );
+
+ // @gate supportsNativeUseSyncExternalStore
+ // @gate experimental
+ it(
+ 'errors during hydration force a client render at the nearest Suspense ' +
+ 'boundary, and during the client render it recovers, then a deeper ' +
+ 'child suspends',
+ async () => {
+ let isClient = false;
+
+ function subscribe() {
+ return () => {};
+ }
+ function getClientSnapshot() {
+ return 'Yay!';
+ }
+
+ // At the time of writing, the only API that exposes whether it's currently
+ // hydrating is the `getServerSnapshot` API, so I'm using that here to
+ // simulate an error during hydration.
+ function getServerSnapshot() {
+ if (isClient) {
+ throw new Error('Hydration error');
+ }
+ return 'Yay!';
+ }
+
+ function Child() {
+ const value = useSyncExternalStore(
+ subscribe,
+ getClientSnapshot,
+ getServerSnapshot,
+ );
+ if (isClient) {
+ readText(value);
+ }
+ Scheduler.unstable_yieldValue(value);
+ return value;
+ }
+
+ const span1Ref = React.createRef();
+ const span2Ref = React.createRef();
+ const span3Ref = React.createRef();
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+ ,
+ writable,
+ );
+ startWriting();
+ });
+ expect(Scheduler).toHaveYielded(['Yay!']);
+
+ const [span1, span2, span3] = container.getElementsByTagName('span');
+
+ // Hydrate the tree. Child will throw during hydration, but not when it
+ // falls back to client rendering.
+ isClient = true;
+ ReactDOM.hydrateRoot(container, );
+
+ expect(Scheduler).toFlushAndYield([]);
+ expect(getVisibleChildren(container)).toEqual(
+
+
+ Loading...
+
+
,
+ );
+
+ await act(async () => {
+ resolveText('Yay!');
+ });
+ expect(Scheduler).toFlushAndYield(['Yay!']);
+ expect(getVisibleChildren(container)).toEqual(
+
+
+ Yay!
+
+
,
+ );
+
+ // The node that's inside the boundary that errored during hydration was
+ // not hydrated.
+ expect(span2Ref.current).not.toBe(span2);
+
+ // But the nodes outside the boundary were.
+ expect(span1Ref.current).toBe(span1);
+ expect(span3Ref.current).toBe(span3);
+ },
+ );
});
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 9ad53ec00f27f..364489f1e01f8 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -74,6 +74,7 @@ import {
ForceUpdateForLegacySuspense,
StaticMask,
ShouldCapture,
+ ForceClientRender,
} from './ReactFiberFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -2081,6 +2082,14 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
prevState,
renderLanes,
);
+ } else if (workInProgress.flags & ForceClientRender) {
+ // Something errored during hydration. Try again without hydrating.
+ workInProgress.flags &= ~ForceClientRender;
+ return retrySuspenseComponentWithoutHydrating(
+ current,
+ workInProgress,
+ renderLanes,
+ );
} else if (
(workInProgress.memoizedState: null | SuspenseState) !== null
) {
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index bef7863638ff0..08e9f38b173d1 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -74,6 +74,7 @@ import {
ForceUpdateForLegacySuspense,
StaticMask,
ShouldCapture,
+ ForceClientRender,
} from './ReactFiberFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -2081,6 +2082,14 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
prevState,
renderLanes,
);
+ } else if (workInProgress.flags & ForceClientRender) {
+ // Something errored during hydration. Try again without hydrating.
+ workInProgress.flags &= ~ForceClientRender;
+ return retrySuspenseComponentWithoutHydrating(
+ current,
+ workInProgress,
+ renderLanes,
+ );
} else if (
(workInProgress.memoizedState: null | SuspenseState) !== null
) {
diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js
index 13f843ad80607..a82278222bf0a 100644
--- a/packages/react-reconciler/src/ReactFiberFlags.js
+++ b/packages/react-reconciler/src/ReactFiberFlags.js
@@ -12,53 +12,54 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
-export const NoFlags = /* */ 0b000000000000000000000000;
-export const PerformedWork = /* */ 0b000000000000000000000001;
+export const NoFlags = /* */ 0b0000000000000000000000000;
+export const PerformedWork = /* */ 0b0000000000000000000000001;
// You can change the rest (and add more).
-export const Placement = /* */ 0b000000000000000000000010;
-export const Update = /* */ 0b000000000000000000000100;
+export const Placement = /* */ 0b0000000000000000000000010;
+export const Update = /* */ 0b0000000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
-export const Deletion = /* */ 0b000000000000000000001000;
-export const ChildDeletion = /* */ 0b000000000000000000010000;
-export const ContentReset = /* */ 0b000000000000000000100000;
-export const Callback = /* */ 0b000000000000000001000000;
-export const DidCapture = /* */ 0b000000000000000010000000;
-export const Ref = /* */ 0b000000000000000100000000;
-export const Snapshot = /* */ 0b000000000000001000000000;
-export const Passive = /* */ 0b000000000000010000000000;
-export const Hydrating = /* */ 0b000000000000100000000000;
+export const Deletion = /* */ 0b0000000000000000000001000;
+export const ChildDeletion = /* */ 0b0000000000000000000010000;
+export const ContentReset = /* */ 0b0000000000000000000100000;
+export const Callback = /* */ 0b0000000000000000001000000;
+export const DidCapture = /* */ 0b0000000000000000010000000;
+export const ForceClientRender = /* */ 0b0000000000000000100000000;
+export const Ref = /* */ 0b0000000000000001000000000;
+export const Snapshot = /* */ 0b0000000000000010000000000;
+export const Passive = /* */ 0b0000000000000100000000000;
+export const Hydrating = /* */ 0b0000000000001000000000000;
export const HydratingAndUpdate = /* */ Hydrating | Update;
-export const Visibility = /* */ 0b000000000001000000000000;
-export const StoreConsistency = /* */ 0b000000000010000000000000;
+export const Visibility = /* */ 0b0000000000010000000000000;
+export const StoreConsistency = /* */ 0b0000000000100000000000000;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
-export const HostEffectMask = /* */ 0b000000000011111111111111;
+export const HostEffectMask = /* */ 0b0000000000111111111111111;
// These are not really side effects, but we still reuse this field.
-export const Incomplete = /* */ 0b000000000100000000000000;
-export const ShouldCapture = /* */ 0b000000001000000000000000;
-export const ForceUpdateForLegacySuspense = /* */ 0b000000010000000000000000;
-export const DidPropagateContext = /* */ 0b000000100000000000000000;
-export const NeedsPropagation = /* */ 0b000001000000000000000000;
+export const Incomplete = /* */ 0b0000000001000000000000000;
+export const ShouldCapture = /* */ 0b0000000010000000000000000;
+export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000;
+export const DidPropagateContext = /* */ 0b0000001000000000000000000;
+export const NeedsPropagation = /* */ 0b0000010000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
-export const RefStatic = /* */ 0b000010000000000000000000;
-export const LayoutStatic = /* */ 0b000100000000000000000000;
-export const PassiveStatic = /* */ 0b001000000000000000000000;
+export const RefStatic = /* */ 0b0000100000000000000000000;
+export const LayoutStatic = /* */ 0b0001000000000000000000000;
+export const PassiveStatic = /* */ 0b0010000000000000000000000;
// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
-export const MountLayoutDev = /* */ 0b010000000000000000000000;
-export const MountPassiveDev = /* */ 0b100000000000000000000000;
+export const MountLayoutDev = /* */ 0b0100000000000000000000000;
+export const MountPassiveDev = /* */ 0b1000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 5b07409a7729f..78e88f125a1aa 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -32,6 +32,7 @@ import {
ShouldCapture,
LifecycleEffectMask,
ForceUpdateForLegacySuspense,
+ ForceClientRender,
} from './ReactFiberFlags';
import {
supportsPersistence,
@@ -78,6 +79,7 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.new';
+import {getIsHydrating} from './ReactFiberHydrationContext.new';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -160,35 +162,264 @@ function createClassErrorUpdate(
return update;
}
-function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
- // Attach a listener to the promise to "ping" the root and retry. But only if
- // one does not already exist for the lanes we're currently rendering (which
- // acts like a "thread ID" here).
- let pingCache = root.pingCache;
- let threadIDs;
- if (pingCache === null) {
- pingCache = root.pingCache = new PossiblyWeakMap();
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- } else {
- threadIDs = pingCache.get(wakeable);
- if (threadIDs === undefined) {
+function attachWakeableListeners(
+ suspenseBoundary: Fiber,
+ root: FiberRoot,
+ wakeable: Wakeable,
+ lanes: Lanes,
+) {
+ // Attach a ping listener
+ //
+ // The data might resolve before we have a chance to commit the fallback. Or,
+ // in the case of a refresh, we'll never commit a fallback. So we need to
+ // attach a listener now. When it resolves ("pings"), we can decide whether to
+ // try rendering the tree again.
+ //
+ // Only attach a listener if one does not already exist for the lanes
+ // we're currently rendering (which acts like a "thread ID" here).
+ //
+ // We only need to do this in concurrent mode. Legacy Suspense always
+ // commits fallbacks synchronously, so there are no pings.
+ if (suspenseBoundary.mode & ConcurrentMode) {
+ let pingCache = root.pingCache;
+ let threadIDs;
+ if (pingCache === null) {
+ pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
+ } else {
+ threadIDs = pingCache.get(wakeable);
+ if (threadIDs === undefined) {
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ }
}
- }
- if (!threadIDs.has(lanes)) {
- // Memoize using the thread ID to prevent redundant listeners.
- threadIDs.add(lanes);
- const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
- if (enableUpdaterTracking) {
- if (isDevToolsPresent) {
- // If we have pending work still, restore the original updaters
- restorePendingUpdaters(root, lanes);
+ if (!threadIDs.has(lanes)) {
+ // Memoize using the thread ID to prevent redundant listeners.
+ threadIDs.add(lanes);
+ const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
+ if (enableUpdaterTracking) {
+ if (isDevToolsPresent) {
+ // If we have pending work still, restore the original updaters
+ restorePendingUpdaters(root, lanes);
+ }
}
+ wakeable.then(ping, ping);
}
- wakeable.then(ping, ping);
}
+
+ // Retry listener
+ //
+ // If the fallback does commit, we need to attach a different type of
+ // listener. This one schedules an update on the Suspense boundary to turn
+ // the fallback state off.
+ //
+ // Stash the wakeable on the boundary fiber so we can access it in the
+ // commit phase.
+ //
+ // When the wakeable resolves, we'll attempt to render the boundary
+ // again ("retry").
+ const wakeables: Set | null = (suspenseBoundary.updateQueue: any);
+ if (wakeables === null) {
+ const updateQueue = (new Set(): any);
+ updateQueue.add(wakeable);
+ suspenseBoundary.updateQueue = updateQueue;
+ } else {
+ wakeables.add(wakeable);
+ }
+}
+
+function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
+ if (enableLazyContextPropagation) {
+ const currentSourceFiber = sourceFiber.alternate;
+ if (currentSourceFiber !== null) {
+ // Since we never visited the children of the suspended component, we
+ // need to propagate the context change now, to ensure that we visit
+ // them during the retry.
+ //
+ // We don't have to do this for errors because we retry errors without
+ // committing in between. So this is specific to Suspense.
+ propagateParentContextChangesToDeferredTree(
+ currentSourceFiber,
+ sourceFiber,
+ rootRenderLanes,
+ );
+ }
+ }
+
+ // Reset the memoizedState to what it was before we attempted to render it.
+ // A legacy mode Suspense quirk, only relevant to hook components.
+ const tag = sourceFiber.tag;
+ if (
+ (sourceFiber.mode & ConcurrentMode) === NoMode &&
+ (tag === FunctionComponent ||
+ tag === ForwardRef ||
+ tag === SimpleMemoComponent)
+ ) {
+ const currentSource = sourceFiber.alternate;
+ if (currentSource) {
+ sourceFiber.updateQueue = currentSource.updateQueue;
+ sourceFiber.memoizedState = currentSource.memoizedState;
+ sourceFiber.lanes = currentSource.lanes;
+ } else {
+ sourceFiber.updateQueue = null;
+ sourceFiber.memoizedState = null;
+ }
+ }
+}
+
+function markNearestSuspenseBoundaryShouldCapture(
+ returnFiber: Fiber,
+ sourceFiber: Fiber,
+ root: FiberRoot,
+ rootRenderLanes: Lanes,
+): Fiber | null {
+ 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;
+
+ // 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 (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;
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+ 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;
+ }
+ // This boundary already captured during this render. Continue to the next
+ // boundary.
+ node = node.return;
+ } while (node !== null);
+
+ // Could not find a Suspense boundary capable of capturing.
+ return null;
}
function throwException(
@@ -213,25 +444,9 @@ function throwException(
typeof value === 'object' &&
typeof value.then === 'function'
) {
- if (enableLazyContextPropagation) {
- const currentSourceFiber = sourceFiber.alternate;
- if (currentSourceFiber !== null) {
- // Since we never visited the children of the suspended component, we
- // need to propagate the context change now, to ensure that we visit
- // them during the retry.
- //
- // We don't have to do this for errors because we retry errors without
- // committing in between. So this is specific to Suspense.
- propagateParentContextChangesToDeferredTree(
- currentSourceFiber,
- sourceFiber,
- rootRenderLanes,
- );
- }
- }
-
- // This is a wakeable.
+ // This is a wakeable. The component suspended.
const wakeable: Wakeable = (value: any);
+ resetSuspendedComponent(sourceFiber, rootRenderLanes);
if (__DEV__) {
if (enableDebugTracing) {
@@ -242,190 +457,54 @@ function throwException(
}
}
- // Reset the memoizedState to what it was before we attempted to render it.
- // A legacy mode Suspense quirk, only relevant to hook components.
- const tag = sourceFiber.tag;
- if (
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
- (tag === FunctionComponent ||
- tag === ForwardRef ||
- tag === SimpleMemoComponent)
- ) {
- const currentSource = sourceFiber.alternate;
- if (currentSource) {
- sourceFiber.updateQueue = currentSource.updateQueue;
- sourceFiber.memoizedState = currentSource.memoizedState;
- sourceFiber.lanes = currentSource.lanes;
- } else {
- sourceFiber.updateQueue = null;
- sourceFiber.memoizedState = null;
- }
- }
-
- const hasInvisibleParentBoundary = hasSuspenseContext(
- suspenseStackCursor.current,
- (InvisibleParentSuspenseContext: SuspenseContext),
- );
-
// Schedule the nearest Suspense to re-render the timed out view.
- let workInProgress = returnFiber;
- do {
- if (
- workInProgress.tag === SuspenseComponent &&
- shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
- ) {
- // Found the nearest boundary.
-
- // Stash the promise on the boundary fiber. If the boundary times out, we'll
- // attach another listener to flip the boundary back to its normal state.
- const wakeables: Set = (workInProgress.updateQueue: any);
- if (wakeables === null) {
- const updateQueue = (new Set(): any);
- updateQueue.add(wakeable);
- workInProgress.updateQueue = updateQueue;
- } else {
- wakeables.add(wakeable);
- }
-
- if ((workInProgress.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 (workInProgress === 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.
- workInProgress.flags |= ShouldCapture;
- } else {
- workInProgress.flags |= DidCapture;
- sourceFiber.flags |= ForceUpdateForLegacySuspense;
-
- // 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 (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 = workInProgress.alternate;
- if (currentSuspenseBoundary === null) {
- const offscreenFiber: Fiber = (workInProgress.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;
- }
- }
- }
-
- 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);
- }
- }
-
- // 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);
- }
- return;
- }
- // 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.
-
- attachPingListener(root, wakeable, rootRenderLanes);
-
- workInProgress.flags |= ShouldCapture;
- // TODO: I think we can remove this, since we now use `DidCapture` in
- // the begin phase to prevent an early bailout.
- workInProgress.lanes = rootRenderLanes;
+ const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
+ if (suspenseBoundary !== null) {
+ attachWakeableListeners(
+ suspenseBoundary,
+ root,
+ wakeable,
+ rootRenderLanes,
+ );
+ return;
+ } else {
+ // No boundary was found. Fallthrough to error mode.
+ // TODO: Use invariant so the message is stripped in prod?
+ value = new Error(
+ (getComponentNameFromFiber(sourceFiber) || 'A React component') +
+ ' suspended while rendering, but no fallback UI was specified.\n' +
+ '\n' +
+ 'Add a component higher in the tree to ' +
+ 'provide a loading indicator or placeholder to display.',
+ );
+ }
+ } else {
+ // This is a regular error, not a Suspense wakeable.
+ if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
+ // 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;
return;
}
- // This boundary already captured during this render. Continue to the next
- // boundary.
- workInProgress = workInProgress.return;
- } while (workInProgress !== null);
- // No boundary was found. Fallthrough to error mode.
- // TODO: Use invariant so the message is stripped in prod?
- value = new Error(
- (getComponentNameFromFiber(sourceFiber) || 'A React component') +
- ' suspended while rendering, but no fallback UI was specified.\n' +
- '\n' +
- 'Add a component higher in the tree to ' +
- 'provide a loading indicator or placeholder to display.',
- );
+ } else {
+ // Otherwise, fall through to the error path.
+ }
}
// We didn't find a boundary that could handle this type of exception. Start
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index dcba4b521aebc..d2d39793a3bc0 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -32,6 +32,7 @@ import {
ShouldCapture,
LifecycleEffectMask,
ForceUpdateForLegacySuspense,
+ ForceClientRender,
} from './ReactFiberFlags';
import {
supportsPersistence,
@@ -78,6 +79,7 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.old';
+import {getIsHydrating} from './ReactFiberHydrationContext.old';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -160,35 +162,264 @@ function createClassErrorUpdate(
return update;
}
-function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
- // Attach a listener to the promise to "ping" the root and retry. But only if
- // one does not already exist for the lanes we're currently rendering (which
- // acts like a "thread ID" here).
- let pingCache = root.pingCache;
- let threadIDs;
- if (pingCache === null) {
- pingCache = root.pingCache = new PossiblyWeakMap();
- threadIDs = new Set();
- pingCache.set(wakeable, threadIDs);
- } else {
- threadIDs = pingCache.get(wakeable);
- if (threadIDs === undefined) {
+function attachWakeableListeners(
+ suspenseBoundary: Fiber,
+ root: FiberRoot,
+ wakeable: Wakeable,
+ lanes: Lanes,
+) {
+ // Attach a ping listener
+ //
+ // The data might resolve before we have a chance to commit the fallback. Or,
+ // in the case of a refresh, we'll never commit a fallback. So we need to
+ // attach a listener now. When it resolves ("pings"), we can decide whether to
+ // try rendering the tree again.
+ //
+ // Only attach a listener if one does not already exist for the lanes
+ // we're currently rendering (which acts like a "thread ID" here).
+ //
+ // We only need to do this in concurrent mode. Legacy Suspense always
+ // commits fallbacks synchronously, so there are no pings.
+ if (suspenseBoundary.mode & ConcurrentMode) {
+ let pingCache = root.pingCache;
+ let threadIDs;
+ if (pingCache === null) {
+ pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
+ } else {
+ threadIDs = pingCache.get(wakeable);
+ if (threadIDs === undefined) {
+ threadIDs = new Set();
+ pingCache.set(wakeable, threadIDs);
+ }
}
- }
- if (!threadIDs.has(lanes)) {
- // Memoize using the thread ID to prevent redundant listeners.
- threadIDs.add(lanes);
- const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
- if (enableUpdaterTracking) {
- if (isDevToolsPresent) {
- // If we have pending work still, restore the original updaters
- restorePendingUpdaters(root, lanes);
+ if (!threadIDs.has(lanes)) {
+ // Memoize using the thread ID to prevent redundant listeners.
+ threadIDs.add(lanes);
+ const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
+ if (enableUpdaterTracking) {
+ if (isDevToolsPresent) {
+ // If we have pending work still, restore the original updaters
+ restorePendingUpdaters(root, lanes);
+ }
}
+ wakeable.then(ping, ping);
}
- wakeable.then(ping, ping);
}
+
+ // Retry listener
+ //
+ // If the fallback does commit, we need to attach a different type of
+ // listener. This one schedules an update on the Suspense boundary to turn
+ // the fallback state off.
+ //
+ // Stash the wakeable on the boundary fiber so we can access it in the
+ // commit phase.
+ //
+ // When the wakeable resolves, we'll attempt to render the boundary
+ // again ("retry").
+ const wakeables: Set | null = (suspenseBoundary.updateQueue: any);
+ if (wakeables === null) {
+ const updateQueue = (new Set(): any);
+ updateQueue.add(wakeable);
+ suspenseBoundary.updateQueue = updateQueue;
+ } else {
+ wakeables.add(wakeable);
+ }
+}
+
+function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
+ if (enableLazyContextPropagation) {
+ const currentSourceFiber = sourceFiber.alternate;
+ if (currentSourceFiber !== null) {
+ // Since we never visited the children of the suspended component, we
+ // need to propagate the context change now, to ensure that we visit
+ // them during the retry.
+ //
+ // We don't have to do this for errors because we retry errors without
+ // committing in between. So this is specific to Suspense.
+ propagateParentContextChangesToDeferredTree(
+ currentSourceFiber,
+ sourceFiber,
+ rootRenderLanes,
+ );
+ }
+ }
+
+ // Reset the memoizedState to what it was before we attempted to render it.
+ // A legacy mode Suspense quirk, only relevant to hook components.
+ const tag = sourceFiber.tag;
+ if (
+ (sourceFiber.mode & ConcurrentMode) === NoMode &&
+ (tag === FunctionComponent ||
+ tag === ForwardRef ||
+ tag === SimpleMemoComponent)
+ ) {
+ const currentSource = sourceFiber.alternate;
+ if (currentSource) {
+ sourceFiber.updateQueue = currentSource.updateQueue;
+ sourceFiber.memoizedState = currentSource.memoizedState;
+ sourceFiber.lanes = currentSource.lanes;
+ } else {
+ sourceFiber.updateQueue = null;
+ sourceFiber.memoizedState = null;
+ }
+ }
+}
+
+function markNearestSuspenseBoundaryShouldCapture(
+ returnFiber: Fiber,
+ sourceFiber: Fiber,
+ root: FiberRoot,
+ rootRenderLanes: Lanes,
+): Fiber | null {
+ 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;
+
+ // 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 (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;
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ // 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);
+ }
+ 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;
+ }
+ // This boundary already captured during this render. Continue to the next
+ // boundary.
+ node = node.return;
+ } while (node !== null);
+
+ // Could not find a Suspense boundary capable of capturing.
+ return null;
}
function throwException(
@@ -213,25 +444,9 @@ function throwException(
typeof value === 'object' &&
typeof value.then === 'function'
) {
- if (enableLazyContextPropagation) {
- const currentSourceFiber = sourceFiber.alternate;
- if (currentSourceFiber !== null) {
- // Since we never visited the children of the suspended component, we
- // need to propagate the context change now, to ensure that we visit
- // them during the retry.
- //
- // We don't have to do this for errors because we retry errors without
- // committing in between. So this is specific to Suspense.
- propagateParentContextChangesToDeferredTree(
- currentSourceFiber,
- sourceFiber,
- rootRenderLanes,
- );
- }
- }
-
- // This is a wakeable.
+ // This is a wakeable. The component suspended.
const wakeable: Wakeable = (value: any);
+ resetSuspendedComponent(sourceFiber, rootRenderLanes);
if (__DEV__) {
if (enableDebugTracing) {
@@ -242,190 +457,54 @@ function throwException(
}
}
- // Reset the memoizedState to what it was before we attempted to render it.
- // A legacy mode Suspense quirk, only relevant to hook components.
- const tag = sourceFiber.tag;
- if (
- (sourceFiber.mode & ConcurrentMode) === NoMode &&
- (tag === FunctionComponent ||
- tag === ForwardRef ||
- tag === SimpleMemoComponent)
- ) {
- const currentSource = sourceFiber.alternate;
- if (currentSource) {
- sourceFiber.updateQueue = currentSource.updateQueue;
- sourceFiber.memoizedState = currentSource.memoizedState;
- sourceFiber.lanes = currentSource.lanes;
- } else {
- sourceFiber.updateQueue = null;
- sourceFiber.memoizedState = null;
- }
- }
-
- const hasInvisibleParentBoundary = hasSuspenseContext(
- suspenseStackCursor.current,
- (InvisibleParentSuspenseContext: SuspenseContext),
- );
-
// Schedule the nearest Suspense to re-render the timed out view.
- let workInProgress = returnFiber;
- do {
- if (
- workInProgress.tag === SuspenseComponent &&
- shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
- ) {
- // Found the nearest boundary.
-
- // Stash the promise on the boundary fiber. If the boundary times out, we'll
- // attach another listener to flip the boundary back to its normal state.
- const wakeables: Set = (workInProgress.updateQueue: any);
- if (wakeables === null) {
- const updateQueue = (new Set(): any);
- updateQueue.add(wakeable);
- workInProgress.updateQueue = updateQueue;
- } else {
- wakeables.add(wakeable);
- }
-
- if ((workInProgress.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 (workInProgress === 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.
- workInProgress.flags |= ShouldCapture;
- } else {
- workInProgress.flags |= DidCapture;
- sourceFiber.flags |= ForceUpdateForLegacySuspense;
-
- // 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 (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 = workInProgress.alternate;
- if (currentSuspenseBoundary === null) {
- const offscreenFiber: Fiber = (workInProgress.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;
- }
- }
- }
-
- 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);
- }
- }
-
- // 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);
- }
- return;
- }
- // 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.
-
- attachPingListener(root, wakeable, rootRenderLanes);
-
- workInProgress.flags |= ShouldCapture;
- // TODO: I think we can remove this, since we now use `DidCapture` in
- // the begin phase to prevent an early bailout.
- workInProgress.lanes = rootRenderLanes;
+ const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
+ returnFiber,
+ sourceFiber,
+ root,
+ rootRenderLanes,
+ );
+ if (suspenseBoundary !== null) {
+ attachWakeableListeners(
+ suspenseBoundary,
+ root,
+ wakeable,
+ rootRenderLanes,
+ );
+ return;
+ } else {
+ // No boundary was found. Fallthrough to error mode.
+ // TODO: Use invariant so the message is stripped in prod?
+ value = new Error(
+ (getComponentNameFromFiber(sourceFiber) || 'A React component') +
+ ' suspended while rendering, but no fallback UI was specified.\n' +
+ '\n' +
+ 'Add a component higher in the tree to ' +
+ 'provide a loading indicator or placeholder to display.',
+ );
+ }
+ } else {
+ // This is a regular error, not a Suspense wakeable.
+ if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
+ // 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;
return;
}
- // This boundary already captured during this render. Continue to the next
- // boundary.
- workInProgress = workInProgress.return;
- } while (workInProgress !== null);
- // No boundary was found. Fallthrough to error mode.
- // TODO: Use invariant so the message is stripped in prod?
- value = new Error(
- (getComponentNameFromFiber(sourceFiber) || 'A React component') +
- ' suspended while rendering, but no fallback UI was specified.\n' +
- '\n' +
- 'Add a component higher in the tree to ' +
- 'provide a loading indicator or placeholder to display.',
- );
+ } else {
+ // Otherwise, fall through to the error path.
+ }
}
// We didn't find a boundary that could handle this type of exception. Start