mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
34ee3919c3
This flag has shipped everywhere, let's clean it up.
690 lines
26 KiB
JavaScript
690 lines
26 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
|
import type {Lane, Lanes} from './ReactFiberLane';
|
|
import type {CapturedValue} from './ReactCapturedValue';
|
|
import type {Update} from './ReactFiberClassUpdateQueue';
|
|
import type {Wakeable} from 'shared/ReactTypes';
|
|
import type {OffscreenQueue} from './ReactFiberActivityComponent';
|
|
import type {RetryQueue} from './ReactFiberSuspenseComponent';
|
|
|
|
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
|
import {
|
|
ClassComponent,
|
|
HostRoot,
|
|
IncompleteClassComponent,
|
|
IncompleteFunctionComponent,
|
|
FunctionComponent,
|
|
ForwardRef,
|
|
SimpleMemoComponent,
|
|
SuspenseComponent,
|
|
OffscreenComponent,
|
|
} from './ReactWorkTags';
|
|
import {
|
|
DidCapture,
|
|
Incomplete,
|
|
NoFlags,
|
|
ShouldCapture,
|
|
LifecycleEffectMask,
|
|
ForceUpdateForLegacySuspense,
|
|
ForceClientRender,
|
|
ScheduleRetry,
|
|
} from './ReactFiberFlags';
|
|
import {NoMode, ConcurrentMode} from './ReactTypeOfMode';
|
|
import {
|
|
enableUpdaterTracking,
|
|
enablePostpone,
|
|
disableLegacyMode,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {createCapturedValueAtFiber} from './ReactCapturedValue';
|
|
import {
|
|
enqueueCapturedUpdate,
|
|
createUpdate,
|
|
CaptureUpdate,
|
|
ForceUpdate,
|
|
enqueueUpdate,
|
|
} from './ReactFiberClassUpdateQueue';
|
|
import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
|
|
import {
|
|
getShellBoundary,
|
|
getSuspenseHandler,
|
|
} from './ReactFiberSuspenseContext';
|
|
import {
|
|
renderDidError,
|
|
queueConcurrentError,
|
|
renderDidSuspendDelayIfPossible,
|
|
markLegacyErrorBoundaryAsFailed,
|
|
isAlreadyFailedLegacyErrorBoundary,
|
|
attachPingListener,
|
|
restorePendingUpdaters,
|
|
renderDidSuspend,
|
|
} from './ReactFiberWorkLoop';
|
|
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext';
|
|
import {logUncaughtError, logCaughtError} from './ReactFiberErrorLogger';
|
|
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
|
|
import {
|
|
SyncLane,
|
|
includesSomeLane,
|
|
mergeLanes,
|
|
pickArbitraryLane,
|
|
} from './ReactFiberLane';
|
|
import {
|
|
getIsHydrating,
|
|
markDidThrowWhileHydratingDEV,
|
|
queueHydrationError,
|
|
HydrationMismatchException,
|
|
} from './ReactFiberHydrationContext';
|
|
import {ConcurrentRoot} from './ReactRootTags';
|
|
import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
|
|
import {REACT_POSTPONE_TYPE} from 'shared/ReactSymbols';
|
|
import {runWithFiberInDEV} from './ReactCurrentFiber';
|
|
import {callComponentDidCatchInDEV} from './ReactFiberCallUserSpace';
|
|
|
|
function createRootErrorUpdate(
|
|
root: FiberRoot,
|
|
errorInfo: CapturedValue<mixed>,
|
|
lane: Lane,
|
|
): Update<mixed> {
|
|
const update = createUpdate(lane);
|
|
// Unmount the root by rendering null.
|
|
update.tag = CaptureUpdate;
|
|
// Caution: React DevTools currently depends on this property
|
|
// being called "element".
|
|
update.payload = {element: null};
|
|
update.callback = () => {
|
|
if (__DEV__) {
|
|
runWithFiberInDEV(errorInfo.source, logUncaughtError, root, errorInfo);
|
|
} else {
|
|
logUncaughtError(root, errorInfo);
|
|
}
|
|
};
|
|
return update;
|
|
}
|
|
|
|
function createClassErrorUpdate(lane: Lane): Update<mixed> {
|
|
const update = createUpdate(lane);
|
|
update.tag = CaptureUpdate;
|
|
return update;
|
|
}
|
|
|
|
function initializeClassErrorUpdate(
|
|
update: Update<mixed>,
|
|
root: FiberRoot,
|
|
fiber: Fiber,
|
|
errorInfo: CapturedValue<mixed>,
|
|
): void {
|
|
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
|
|
if (typeof getDerivedStateFromError === 'function') {
|
|
const error = errorInfo.value;
|
|
update.payload = () => {
|
|
return getDerivedStateFromError(error);
|
|
};
|
|
update.callback = () => {
|
|
if (__DEV__) {
|
|
markFailedErrorBoundaryForHotReloading(fiber);
|
|
}
|
|
if (__DEV__) {
|
|
runWithFiberInDEV(
|
|
errorInfo.source,
|
|
logCaughtError,
|
|
root,
|
|
fiber,
|
|
errorInfo,
|
|
);
|
|
} else {
|
|
logCaughtError(root, fiber, errorInfo);
|
|
}
|
|
};
|
|
}
|
|
|
|
const inst = fiber.stateNode;
|
|
if (inst !== null && typeof inst.componentDidCatch === 'function') {
|
|
// $FlowFixMe[missing-this-annot]
|
|
update.callback = function callback() {
|
|
if (__DEV__) {
|
|
markFailedErrorBoundaryForHotReloading(fiber);
|
|
}
|
|
if (__DEV__) {
|
|
runWithFiberInDEV(
|
|
errorInfo.source,
|
|
logCaughtError,
|
|
root,
|
|
fiber,
|
|
errorInfo,
|
|
);
|
|
} else {
|
|
logCaughtError(root, fiber, errorInfo);
|
|
}
|
|
if (typeof getDerivedStateFromError !== 'function') {
|
|
// To preserve the preexisting retry behavior of error boundaries,
|
|
// we keep track of which ones already failed during this batch.
|
|
// This gets reset before we yield back to the browser.
|
|
// TODO: Warn in strict mode if getDerivedStateFromError is
|
|
// not defined.
|
|
markLegacyErrorBoundaryAsFailed(this);
|
|
}
|
|
if (__DEV__) {
|
|
callComponentDidCatchInDEV(this, errorInfo);
|
|
} else {
|
|
const error = errorInfo.value;
|
|
const stack = errorInfo.stack;
|
|
this.componentDidCatch(error, {
|
|
componentStack: stack !== null ? stack : '',
|
|
});
|
|
}
|
|
if (__DEV__) {
|
|
if (typeof getDerivedStateFromError !== 'function') {
|
|
// If componentDidCatch is the only error boundary method defined,
|
|
// then it needs to call setState to recover from errors.
|
|
// If no state update is scheduled then the boundary will swallow the error.
|
|
if (!includesSomeLane(fiber.lanes, (SyncLane: Lane))) {
|
|
console.error(
|
|
'%s: Error boundaries should implement getDerivedStateFromError(). ' +
|
|
'In that method, return a state update to display an error message or fallback UI.',
|
|
getComponentNameFromFiber(fiber) || 'Unknown',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
|
|
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 (
|
|
!disableLegacyMode &&
|
|
(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 markSuspenseBoundaryShouldCapture(
|
|
suspenseBoundary: Fiber,
|
|
returnFiber: Fiber | null,
|
|
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 (
|
|
!disableLegacyMode &&
|
|
(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 (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(SyncLane);
|
|
update.tag = ForceUpdate;
|
|
enqueueUpdate(sourceFiber, update, SyncLane);
|
|
}
|
|
} else if (sourceFiber.tag === FunctionComponent) {
|
|
const currentSourceFiber = sourceFiber.alternate;
|
|
if (currentSourceFiber === null) {
|
|
// This is a new mount. Change the tag so it's not mistaken for a
|
|
// completed function component.
|
|
sourceFiber.tag = IncompleteFunctionComponent;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
function throwException(
|
|
root: FiberRoot,
|
|
returnFiber: Fiber | null,
|
|
sourceFiber: Fiber,
|
|
value: mixed,
|
|
rootRenderLanes: Lanes,
|
|
): boolean {
|
|
// The source fiber did not complete.
|
|
sourceFiber.flags |= Incomplete;
|
|
|
|
if (enableUpdaterTracking) {
|
|
if (isDevToolsPresent) {
|
|
// If we have pending work still, restore the original updaters
|
|
restorePendingUpdaters(root, rootRenderLanes);
|
|
}
|
|
}
|
|
|
|
if (value !== null && typeof value === 'object') {
|
|
if (enablePostpone && value.$$typeof === REACT_POSTPONE_TYPE) {
|
|
// Act as if this is an infinitely suspending promise.
|
|
value = {then: function () {}};
|
|
}
|
|
if (typeof value.then === 'function') {
|
|
// This is a wakeable. The component suspended.
|
|
const wakeable: Wakeable = (value: any);
|
|
resetSuspendedComponent(sourceFiber, rootRenderLanes);
|
|
|
|
if (__DEV__) {
|
|
if (
|
|
getIsHydrating() &&
|
|
(disableLegacyMode || sourceFiber.mode & ConcurrentMode)
|
|
) {
|
|
markDidThrowWhileHydratingDEV();
|
|
}
|
|
}
|
|
|
|
// Mark the nearest Suspense boundary to switch to rendering a fallback.
|
|
const suspenseBoundary = getSuspenseHandler();
|
|
if (suspenseBoundary !== null) {
|
|
switch (suspenseBoundary.tag) {
|
|
case SuspenseComponent: {
|
|
// If this suspense boundary is not already showing a fallback, mark
|
|
// the in-progress render as suspended. We try to perform this logic
|
|
// as soon as soon as possible during the render phase, so the work
|
|
// loop can know things like whether it's OK to switch to other tasks,
|
|
// or whether it can wait for data to resolve before continuing.
|
|
// TODO: Most of these checks are already performed when entering a
|
|
// Suspense boundary. We should track the information on the stack so
|
|
// we don't have to recompute it on demand. This would also allow us
|
|
// to unify with `use` which needs to perform this logic even sooner,
|
|
// before `throwException` is called.
|
|
if (disableLegacyMode || sourceFiber.mode & ConcurrentMode) {
|
|
if (getShellBoundary() === null) {
|
|
// Suspended in the "shell" of the app. This is an undesirable
|
|
// loading state. We should avoid committing this tree.
|
|
renderDidSuspendDelayIfPossible();
|
|
} else {
|
|
// If we suspended deeper than the shell, we don't need to delay
|
|
// the commmit. However, we still call renderDidSuspend if this is
|
|
// a new boundary, to tell the work loop that a new fallback has
|
|
// appeared during this render.
|
|
// TODO: Theoretically we should be able to delete this branch.
|
|
// It's currently used for two things: 1) to throttle the
|
|
// appearance of successive loading states, and 2) in
|
|
// SuspenseList, to determine whether the children include any
|
|
// pending fallbacks. For 1, we should apply throttling to all
|
|
// retries, not just ones that render an additional fallback. For
|
|
// 2, we should check subtreeFlags instead. Then we can delete
|
|
// this branch.
|
|
const current = suspenseBoundary.alternate;
|
|
if (current === null) {
|
|
renderDidSuspend();
|
|
}
|
|
}
|
|
}
|
|
|
|
suspenseBoundary.flags &= ~ForceClientRender;
|
|
markSuspenseBoundaryShouldCapture(
|
|
suspenseBoundary,
|
|
returnFiber,
|
|
sourceFiber,
|
|
root,
|
|
rootRenderLanes,
|
|
);
|
|
// 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").
|
|
|
|
// Check if this is a Suspensey resource. We do not attach retry
|
|
// listeners to these, because we don't actually need them for
|
|
// rendering. Only for committing. Instead, if a fallback commits
|
|
// and the only thing that suspended was a Suspensey resource, we
|
|
// retry immediately.
|
|
// TODO: Refactor throwException so that we don't have to do this type
|
|
// check. The caller already knows what the cause was.
|
|
const isSuspenseyResource =
|
|
wakeable === noopSuspenseyCommitThenable;
|
|
if (isSuspenseyResource) {
|
|
suspenseBoundary.flags |= ScheduleRetry;
|
|
} else {
|
|
const retryQueue: RetryQueue | null =
|
|
(suspenseBoundary.updateQueue: any);
|
|
if (retryQueue === null) {
|
|
suspenseBoundary.updateQueue = new Set([wakeable]);
|
|
} else {
|
|
retryQueue.add(wakeable);
|
|
}
|
|
|
|
// We only attach ping listeners in concurrent mode. Legacy
|
|
// Suspense always commits fallbacks synchronously, so there are
|
|
// no pings.
|
|
if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) {
|
|
attachPingListener(root, wakeable, rootRenderLanes);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
case OffscreenComponent: {
|
|
if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) {
|
|
suspenseBoundary.flags |= ShouldCapture;
|
|
const isSuspenseyResource =
|
|
wakeable === noopSuspenseyCommitThenable;
|
|
if (isSuspenseyResource) {
|
|
suspenseBoundary.flags |= ScheduleRetry;
|
|
} else {
|
|
const offscreenQueue: OffscreenQueue | null =
|
|
(suspenseBoundary.updateQueue: any);
|
|
if (offscreenQueue === null) {
|
|
const newOffscreenQueue: OffscreenQueue = {
|
|
transitions: null,
|
|
markerInstances: null,
|
|
retryQueue: new Set([wakeable]),
|
|
};
|
|
suspenseBoundary.updateQueue = newOffscreenQueue;
|
|
} else {
|
|
const retryQueue = offscreenQueue.retryQueue;
|
|
if (retryQueue === null) {
|
|
offscreenQueue.retryQueue = new Set([wakeable]);
|
|
} else {
|
|
retryQueue.add(wakeable);
|
|
}
|
|
}
|
|
|
|
attachPingListener(root, wakeable, rootRenderLanes);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
|
|
'is a bug in React.',
|
|
);
|
|
} else {
|
|
// No boundary was found. Unless this is a sync update, this is OK.
|
|
// We can suspend and wait for more data to arrive.
|
|
|
|
if (disableLegacyMode || root.tag === ConcurrentRoot) {
|
|
// In a concurrent root, suspending without a Suspense boundary is
|
|
// allowed. It will suspend indefinitely without committing.
|
|
//
|
|
// TODO: Should we have different behavior for discrete updates? What
|
|
// about flushSync? Maybe it should put the tree into an inert state,
|
|
// and potentially log a warning. Revisit this for a future release.
|
|
attachPingListener(root, wakeable, rootRenderLanes);
|
|
renderDidSuspendDelayIfPossible();
|
|
return false;
|
|
} else {
|
|
// In a legacy root, suspending without a boundary is always an error.
|
|
const uncaughtSuspenseError = new Error(
|
|
'A component suspended while responding to synchronous input. This ' +
|
|
'will cause the UI to be replaced with a loading indicator. To ' +
|
|
'fix, updates that suspend should be wrapped ' +
|
|
'with startTransition.',
|
|
);
|
|
value = uncaughtSuspenseError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is a regular error, not a Suspense wakeable.
|
|
if (
|
|
getIsHydrating() &&
|
|
(disableLegacyMode || sourceFiber.mode & ConcurrentMode)
|
|
) {
|
|
markDidThrowWhileHydratingDEV();
|
|
const suspenseBoundary = getSuspenseHandler();
|
|
// 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.
|
|
if (suspenseBoundary !== null) {
|
|
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,
|
|
);
|
|
|
|
// Even though the user may not be affected by this error, we should
|
|
// still log it so it can be fixed.
|
|
if (value !== HydrationMismatchException) {
|
|
const wrapperError = new Error(
|
|
'There was an error while hydrating but React was able to recover by ' +
|
|
'instead client rendering from the nearest Suspense boundary.',
|
|
{cause: value},
|
|
);
|
|
queueHydrationError(
|
|
createCapturedValueAtFiber(wrapperError, sourceFiber),
|
|
);
|
|
}
|
|
return false;
|
|
} else {
|
|
if (value !== HydrationMismatchException) {
|
|
const wrapperError = new Error(
|
|
'There was an error while hydrating but React was able to recover by ' +
|
|
'instead client rendering the entire root.',
|
|
{cause: value},
|
|
);
|
|
queueHydrationError(
|
|
createCapturedValueAtFiber(wrapperError, sourceFiber),
|
|
);
|
|
}
|
|
const workInProgress: Fiber = (root.current: any).alternate;
|
|
// Schedule an update at the root to log the error but this shouldn't
|
|
// actually happen because we should recover.
|
|
workInProgress.flags |= ShouldCapture;
|
|
const lane = pickArbitraryLane(rootRenderLanes);
|
|
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
|
const rootErrorInfo = createCapturedValueAtFiber(value, sourceFiber);
|
|
const update = createRootErrorUpdate(
|
|
workInProgress.stateNode,
|
|
rootErrorInfo, // This should never actually get logged due to the recovery.
|
|
lane,
|
|
);
|
|
enqueueCapturedUpdate(workInProgress, update);
|
|
renderDidError();
|
|
return false;
|
|
}
|
|
} else {
|
|
// Otherwise, fall through to the error path.
|
|
}
|
|
|
|
const wrapperError = new Error(
|
|
'There was an error during concurrent rendering but React was able to recover by ' +
|
|
'instead synchronously rendering the entire root.',
|
|
{cause: value},
|
|
);
|
|
queueConcurrentError(createCapturedValueAtFiber(wrapperError, sourceFiber));
|
|
renderDidError();
|
|
|
|
// We didn't find a boundary that could handle this type of exception. Start
|
|
// over and traverse parent path again, this time treating the exception
|
|
// as an error.
|
|
|
|
if (returnFiber === null) {
|
|
// There's no return fiber, which means the root errored. This should never
|
|
// happen. Return `true` to trigger a fatal error (panic).
|
|
return true;
|
|
}
|
|
|
|
const errorInfo = createCapturedValueAtFiber(value, sourceFiber);
|
|
let workInProgress: Fiber = returnFiber;
|
|
do {
|
|
switch (workInProgress.tag) {
|
|
case HostRoot: {
|
|
workInProgress.flags |= ShouldCapture;
|
|
const lane = pickArbitraryLane(rootRenderLanes);
|
|
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
|
const update = createRootErrorUpdate(
|
|
workInProgress.stateNode,
|
|
errorInfo,
|
|
lane,
|
|
);
|
|
enqueueCapturedUpdate(workInProgress, update);
|
|
return false;
|
|
}
|
|
case ClassComponent:
|
|
// Capture and retry
|
|
const ctor = workInProgress.type;
|
|
const instance = workInProgress.stateNode;
|
|
if (
|
|
(workInProgress.flags & DidCapture) === NoFlags &&
|
|
(typeof ctor.getDerivedStateFromError === 'function' ||
|
|
(instance !== null &&
|
|
typeof instance.componentDidCatch === 'function' &&
|
|
!isAlreadyFailedLegacyErrorBoundary(instance)))
|
|
) {
|
|
workInProgress.flags |= ShouldCapture;
|
|
const lane = pickArbitraryLane(rootRenderLanes);
|
|
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
|
|
// Schedule the error boundary to re-render using updated state
|
|
const update = createClassErrorUpdate(lane);
|
|
initializeClassErrorUpdate(update, root, workInProgress, errorInfo);
|
|
enqueueCapturedUpdate(workInProgress, update);
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
|
workInProgress = workInProgress.return;
|
|
} while (workInProgress !== null);
|
|
|
|
return false;
|
|
}
|
|
|
|
export {
|
|
throwException,
|
|
createRootErrorUpdate,
|
|
createClassErrorUpdate,
|
|
initializeClassErrorUpdate,
|
|
};
|