main
js 712 lines 26.9 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {Fiber, FiberRoot} from './ReactInternalTypes';
11 import type {Lane, Lanes} from './ReactFiberLane';
12 import type {CapturedValue} from './ReactCapturedValue';
13 import type {Update} from './ReactFiberClassUpdateQueue';
14 import type {Wakeable} from 'shared/ReactTypes';
15 import type {
16 OffscreenQueue,
17 OffscreenState,
18 } from './ReactFiberOffscreenComponent';
19 import type {RetryQueue} from './ReactFiberSuspenseComponent';
20
21 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
22 import {
23 ClassComponent,
24 HostRoot,
25 IncompleteClassComponent,
26 IncompleteFunctionComponent,
27 FunctionComponent,
28 ForwardRef,
29 SimpleMemoComponent,
30 ActivityComponent,
31 SuspenseComponent,
32 OffscreenComponent,
33 SuspenseListComponent,
34 } from './ReactWorkTags';
35 import {
36 DidCapture,
37 Incomplete,
38 NoFlags,
39 ShouldCapture,
40 LifecycleEffectMask,
41 ForceUpdateForLegacySuspense,
42 ForceClientRender,
43 ScheduleRetry,
44 } from './ReactFiberFlags';
45 import {NoMode, ConcurrentMode} from './ReactTypeOfMode';
46 import {
47 enableUpdaterTracking,
48 disableLegacyMode,
49 } from 'shared/ReactFeatureFlags';
50 import {createCapturedValueAtFiber} from './ReactCapturedValue';
51 import {
52 enqueueCapturedUpdate,
53 createUpdate,
54 CaptureUpdate,
55 ForceUpdate,
56 enqueueUpdate,
57 } from './ReactFiberClassUpdateQueue';
58 import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading';
59 import {
60 getShellBoundary,
61 getSuspenseHandler,
62 } from './ReactFiberSuspenseContext';
63 import {
64 renderDidError,
65 queueConcurrentError,
66 renderDidSuspendDelayIfPossible,
67 markLegacyErrorBoundaryAsFailed,
68 isAlreadyFailedLegacyErrorBoundary,
69 attachPingListener,
70 restorePendingUpdaters,
71 renderDidSuspend,
72 } from './ReactFiberWorkLoop';
73 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext';
74 import {logUncaughtError, logCaughtError} from './ReactFiberErrorLogger';
75 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
76 import {
77 SyncLane,
78 includesSomeLane,
79 mergeLanes,
80 pickArbitraryLane,
81 } from './ReactFiberLane';
82 import {
83 getIsHydrating,
84 markDidThrowWhileHydratingDEV,
85 queueHydrationError,
86 HydrationMismatchException,
87 } from './ReactFiberHydrationContext';
88 import {ConcurrentRoot} from './ReactRootTags';
89 import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
90 import {runWithFiberInDEV} from './ReactCurrentFiber';
91 import {callComponentDidCatchInDEV} from './ReactFiberCallUserSpace';
92
93 function createRootErrorUpdate(
94 root: FiberRoot,
95 errorInfo: CapturedValue<mixed>,
96 lane: Lane,
97 ): Update<mixed> {
98 const update = createUpdate(lane);
99 // Unmount the root by rendering null.
100 update.tag = CaptureUpdate;
101 // Caution: React DevTools currently depends on this property
102 // being called "element".
103 update.payload = {element: null};
104 update.callback = () => {
105 if (__DEV__) {
106 runWithFiberInDEV(errorInfo.source, logUncaughtError, root, errorInfo);
107 } else {
108 logUncaughtError(root, errorInfo);
109 }
110 };
111 return update;
112 }
113
114 function createClassErrorUpdate(lane: Lane): Update<mixed> {
115 const update = createUpdate(lane);
116 update.tag = CaptureUpdate;
117 return update;
118 }
119
120 function initializeClassErrorUpdate(
121 update: Update<mixed>,
122 root: FiberRoot,
123 fiber: Fiber,
124 errorInfo: CapturedValue<mixed>,
125 ): void {
126 const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
127 if (typeof getDerivedStateFromError === 'function') {
128 const error = errorInfo.value;
129 update.payload = () => {
130 return getDerivedStateFromError(error);
131 };
132 update.callback = () => {
133 if (__DEV__) {
134 markFailedErrorBoundaryForHotReloading(fiber);
135 }
136 if (__DEV__) {
137 runWithFiberInDEV(
138 errorInfo.source,
139 logCaughtError,
140 root,
141 fiber,
142 errorInfo,
143 );
144 } else {
145 logCaughtError(root, fiber, errorInfo);
146 }
147 };
148 }
149
150 const inst = fiber.stateNode;
151 if (inst !== null && typeof inst.componentDidCatch === 'function') {
152 // $FlowFixMe[missing-this-annot]
153 update.callback = function callback() {
154 if (__DEV__) {
155 markFailedErrorBoundaryForHotReloading(fiber);
156 }
157 if (__DEV__) {
158 runWithFiberInDEV(
159 errorInfo.source,
160 logCaughtError,
161 root,
162 fiber,
163 errorInfo,
164 );
165 } else {
166 logCaughtError(root, fiber, errorInfo);
167 }
168 if (typeof getDerivedStateFromError !== 'function') {
169 // To preserve the preexisting retry behavior of error boundaries,
170 // we keep track of which ones already failed during this batch.
171 // This gets reset before we yield back to the browser.
172 // TODO: Warn in strict mode if getDerivedStateFromError is
173 // not defined.
174 markLegacyErrorBoundaryAsFailed(this);
175 }
176 if (__DEV__) {
177 callComponentDidCatchInDEV(this, errorInfo);
178 } else {
179 const error = errorInfo.value;
180 const stack = errorInfo.stack;
181 this.componentDidCatch(error, {
182 componentStack: stack !== null ? stack : '',
183 });
184 }
185 if (__DEV__) {
186 if (typeof getDerivedStateFromError !== 'function') {
187 // If componentDidCatch is the only error boundary method defined,
188 // then it needs to call setState to recover from errors.
189 // If no state update is scheduled then the boundary will swallow the error.
190 if (!includesSomeLane(fiber.lanes, SyncLane as Lane)) {
191 console.error(
192 '%s: Error boundaries should implement getDerivedStateFromError(). ' +
193 'In that method, return a state update to display an error message or fallback UI.',
194 getComponentNameFromFiber(fiber) || 'Unknown',
195 );
196 }
197 }
198 }
199 };
200 }
201 }
202
203 function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
204 const currentSourceFiber = sourceFiber.alternate;
205 if (currentSourceFiber !== null) {
206 // Since we never visited the children of the suspended component, we
207 // need to propagate the context change now, to ensure that we visit
208 // them during the retry.
209 //
210 // We don't have to do this for errors because we retry errors without
211 // committing in between. So this is specific to Suspense.
212 propagateParentContextChangesToDeferredTree(
213 currentSourceFiber,
214 sourceFiber,
215 rootRenderLanes,
216 );
217 }
218
219 // Reset the memoizedState to what it was before we attempted to render it.
220 // A legacy mode Suspense quirk, only relevant to hook components.
221 const tag = sourceFiber.tag;
222 if (
223 !disableLegacyMode &&
224 (sourceFiber.mode & ConcurrentMode) === NoMode &&
225 (tag === FunctionComponent ||
226 tag === ForwardRef ||
227 tag === SimpleMemoComponent)
228 ) {
229 const currentSource = sourceFiber.alternate;
230 if (currentSource) {
231 sourceFiber.updateQueue = currentSource.updateQueue;
232 sourceFiber.memoizedState = currentSource.memoizedState;
233 sourceFiber.lanes = currentSource.lanes;
234 } else {
235 sourceFiber.updateQueue = null;
236 sourceFiber.memoizedState = null;
237 }
238 }
239 }
240
241 function markSuspenseBoundaryShouldCapture(
242 suspenseBoundary: Fiber,
243 returnFiber: Fiber | null,
244 sourceFiber: Fiber,
245 root: FiberRoot,
246 rootRenderLanes: Lanes,
247 ): Fiber | null {
248 // This marks a Suspense boundary so that when we're unwinding the stack,
249 // it captures the suspended "exception" and does a second (fallback) pass.
250 if (
251 !disableLegacyMode &&
252 (suspenseBoundary.mode & ConcurrentMode) === NoMode
253 ) {
254 // Legacy Mode Suspense
255 //
256 // If the boundary is in legacy mode, we should *not*
257 // suspend the commit. Pretend as if the suspended component rendered
258 // null and keep rendering. When the Suspense boundary completes,
259 // we'll do a second pass to render the fallback.
260 if (suspenseBoundary === returnFiber) {
261 // Special case where we suspended while reconciling the children of
262 // a Suspense boundary's inner Offscreen wrapper fiber. This happens
263 // when a React.lazy component is a direct child of a
264 // Suspense boundary.
265 //
266 // Suspense boundaries are implemented as multiple fibers, but they
267 // are a single conceptual unit. The legacy mode behavior where we
268 // pretend the suspended fiber committed as `null` won't work,
269 // because in this case the "suspended" fiber is the inner
270 // Offscreen wrapper.
271 //
272 // Because the contents of the boundary haven't started rendering
273 // yet (i.e. nothing in the tree has partially rendered) we can
274 // switch to the regular, concurrent mode behavior: mark the
275 // boundary with ShouldCapture and enter the unwind phase.
276 suspenseBoundary.flags |= ShouldCapture;
277 } else {
278 suspenseBoundary.flags |= DidCapture;
279 sourceFiber.flags |= ForceUpdateForLegacySuspense;
280
281 // We're going to commit this fiber even though it didn't complete.
282 // But we shouldn't call any lifecycle methods or callbacks. Remove
283 // all lifecycle effect tags.
284 sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
285
286 if (sourceFiber.tag === ClassComponent) {
287 const currentSourceFiber = sourceFiber.alternate;
288 if (currentSourceFiber === null) {
289 // This is a new mount. Change the tag so it's not mistaken for a
290 // completed class component. For example, we should not call
291 // componentWillUnmount if it is deleted.
292 sourceFiber.tag = IncompleteClassComponent;
293 } else {
294 // When we try rendering again, we should not reuse the current fiber,
295 // since it's known to be in an inconsistent state. Use a force update to
296 // prevent a bail out.
297 const update = createUpdate(SyncLane);
298 update.tag = ForceUpdate;
299 enqueueUpdate(sourceFiber, update, SyncLane);
300 }
301 } else if (sourceFiber.tag === FunctionComponent) {
302 const currentSourceFiber = sourceFiber.alternate;
303 if (currentSourceFiber === null) {
304 // This is a new mount. Change the tag so it's not mistaken for a
305 // completed function component.
306 sourceFiber.tag = IncompleteFunctionComponent;
307 }
308 }
309
310 // The source fiber did not complete. Mark it with Sync priority to
311 // indicate that it still has pending work.
312 sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
313 }
314 return suspenseBoundary;
315 }
316 // Confirmed that the boundary is in a concurrent mode tree. Continue
317 // with the normal suspend path.
318 //
319 // After this we'll use a set of heuristics to determine whether this
320 // render pass will run to completion or restart or "suspend" the commit.
321 // The actual logic for this is spread out in different places.
322 //
323 // This first principle is that if we're going to suspend when we complete
324 // a root, then we should also restart if we get an update or ping that
325 // might unsuspend it, and vice versa. The only reason to suspend is
326 // because you think you might want to restart before committing. However,
327 // it doesn't make sense to restart only while in the period we're suspended.
328 //
329 // Restarting too aggressively is also not good because it starves out any
330 // intermediate loading state. So we use heuristics to determine when.
331
332 // Suspense Heuristics
333 //
334 // If nothing threw a Promise or all the same fallbacks are already showing,
335 // then don't suspend/restart.
336 //
337 // If this is an initial render of a new tree of Suspense boundaries and
338 // those trigger a fallback, then don't suspend/restart. We want to ensure
339 // that we can show the initial loading state as quickly as possible.
340 //
341 // If we hit a "Delayed" case, such as when we'd switch from content back into
342 // a fallback, then we should always suspend/restart. Transitions apply
343 // to this case. If none is defined, JND is used instead.
344 //
345 // If we're already showing a fallback and it gets "retried", allowing us to show
346 // another level, but there's still an inner boundary that would show a fallback,
347 // then we suspend/restart for 500ms since the last time we showed a fallback
348 // anywhere in the tree. This effectively throttles progressive loading into a
349 // consistent train of commits. This also gives us an opportunity to restart to
350 // get to the completed state slightly earlier.
351 //
352 // If there's ambiguity due to batching it's resolved in preference of:
353 // 1) "delayed", 2) "initial render", 3) "retry".
354 //
355 // We want to ensure that a "busy" state doesn't get force committed. We want to
356 // ensure that new initial loading states can commit as soon as possible.
357 suspenseBoundary.flags |= ShouldCapture;
358 // TODO: I think we can remove this, since we now use `DidCapture` in
359 // the begin phase to prevent an early bailout.
360 suspenseBoundary.lanes = rootRenderLanes;
361 return suspenseBoundary;
362 }
363
364 function throwException(
365 root: FiberRoot,
366 returnFiber: Fiber | null,
367 sourceFiber: Fiber,
368 value: mixed,
369 rootRenderLanes: Lanes,
370 ): boolean {
371 // The source fiber did not complete.
372 sourceFiber.flags |= Incomplete;
373
374 if (enableUpdaterTracking) {
375 if (isDevToolsPresent) {
376 // If we have pending work still, restore the original updaters
377 restorePendingUpdaters(root, rootRenderLanes);
378 }
379 }
380
381 if (value !== null && typeof value === 'object') {
382 if (typeof value.then === 'function') {
383 // This is a wakeable. The component suspended.
384 const wakeable: Wakeable = value as any;
385 resetSuspendedComponent(sourceFiber, rootRenderLanes);
386
387 if (__DEV__) {
388 if (
389 getIsHydrating() &&
390 (disableLegacyMode || sourceFiber.mode & ConcurrentMode)
391 ) {
392 markDidThrowWhileHydratingDEV();
393 }
394 }
395
396 // Mark the nearest Suspense boundary to switch to rendering a fallback.
397 const suspenseBoundary = getSuspenseHandler();
398 if (suspenseBoundary !== null) {
399 switch (suspenseBoundary.tag) {
400 case ActivityComponent:
401 case SuspenseComponent:
402 case SuspenseListComponent: {
403 // If this suspense/activity boundary is not already showing a fallback, mark
404 // the in-progress render as suspended. We try to perform this logic
405 // as soon as soon as possible during the render phase, so the work
406 // loop can know things like whether it's OK to switch to other tasks,
407 // or whether it can wait for data to resolve before continuing.
408 // TODO: Most of these checks are already performed when entering a
409 // Suspense boundary. We should track the information on the stack so
410 // we don't have to recompute it on demand. This would also allow us
411 // to unify with `use` which needs to perform this logic even sooner,
412 // before `throwException` is called.
413 if (disableLegacyMode || sourceFiber.mode & ConcurrentMode) {
414 if (getShellBoundary() === null) {
415 // Suspended in the "shell" of the app. This is an undesirable
416 // loading state. We should avoid committing this tree.
417 renderDidSuspendDelayIfPossible();
418 } else {
419 // If we suspended deeper than the shell, we don't need to delay
420 // the commmit. However, we still call renderDidSuspend if this is
421 // a new boundary, to tell the work loop that a new fallback has
422 // appeared during this render.
423 // TODO: Theoretically we should be able to delete this branch.
424 // It's currently used for two things: 1) to throttle the
425 // appearance of successive loading states, and 2) in
426 // SuspenseList, to determine whether the children include any
427 // pending fallbacks. For 1, we should apply throttling to all
428 // retries, not just ones that render an additional fallback. For
429 // 2, we should check subtreeFlags instead. Then we can delete
430 // this branch.
431 const current = suspenseBoundary.alternate;
432 if (current === null) {
433 renderDidSuspend();
434 }
435 }
436 }
437
438 suspenseBoundary.flags &= ~ForceClientRender;
439 markSuspenseBoundaryShouldCapture(
440 suspenseBoundary,
441 returnFiber,
442 sourceFiber,
443 root,
444 rootRenderLanes,
445 );
446 // Retry listener
447 //
448 // If the fallback does commit, we need to attach a different type of
449 // listener. This one schedules an update on the Suspense boundary to
450 // turn the fallback state off.
451 //
452 // Stash the wakeable on the boundary fiber so we can access it in the
453 // commit phase.
454 //
455 // When the wakeable resolves, we'll attempt to render the boundary
456 // again ("retry").
457
458 // Check if this is a Suspensey resource. We do not attach retry
459 // listeners to these, because we don't actually need them for
460 // rendering. Only for committing. Instead, if a fallback commits
461 // and the only thing that suspended was a Suspensey resource, we
462 // retry immediately.
463 // TODO: Refactor throwException so that we don't have to do this type
464 // check. The caller already knows what the cause was.
465 const isSuspenseyResource =
466 wakeable === noopSuspenseyCommitThenable;
467 if (isSuspenseyResource) {
468 suspenseBoundary.flags |= ScheduleRetry;
469 } else {
470 const retryQueue: RetryQueue | null =
471 suspenseBoundary.updateQueue as any;
472 if (retryQueue === null) {
473 suspenseBoundary.updateQueue = new Set([wakeable]);
474 } else {
475 retryQueue.add(wakeable);
476 }
477
478 // We only attach ping listeners in concurrent mode. Legacy
479 // Suspense always commits fallbacks synchronously, so there are
480 // no pings.
481 if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) {
482 attachPingListener(root, wakeable, rootRenderLanes);
483 }
484 }
485 return false;
486 }
487 case OffscreenComponent: {
488 if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) {
489 suspenseBoundary.flags |= ShouldCapture;
490 const isSuspenseyResource =
491 wakeable === noopSuspenseyCommitThenable;
492 if (isSuspenseyResource) {
493 suspenseBoundary.flags |= ScheduleRetry;
494 } else {
495 const offscreenQueue: OffscreenQueue | null =
496 suspenseBoundary.updateQueue as any;
497 if (offscreenQueue === null) {
498 const newOffscreenQueue: OffscreenQueue = {
499 transitions: null,
500 markerInstances: null,
501 retryQueue: new Set([wakeable]),
502 };
503 suspenseBoundary.updateQueue = newOffscreenQueue;
504 } else {
505 const retryQueue = offscreenQueue.retryQueue;
506 if (retryQueue === null) {
507 offscreenQueue.retryQueue = new Set([wakeable]);
508 } else {
509 retryQueue.add(wakeable);
510 }
511 }
512
513 attachPingListener(root, wakeable, rootRenderLanes);
514 }
515 return false;
516 }
517 }
518 }
519 throw new Error(
520 `Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
521 'is a bug in React.',
522 );
523 } else {
524 // No boundary was found. Unless this is a sync update, this is OK.
525 // We can suspend and wait for more data to arrive.
526
527 if (disableLegacyMode || root.tag === ConcurrentRoot) {
528 // In a concurrent root, suspending without a Suspense boundary is
529 // allowed. It will suspend indefinitely without committing.
530 //
531 // TODO: Should we have different behavior for discrete updates? What
532 // about flushSync? Maybe it should put the tree into an inert state,
533 // and potentially log a warning. Revisit this for a future release.
534 attachPingListener(root, wakeable, rootRenderLanes);
535 renderDidSuspendDelayIfPossible();
536 return false;
537 } else {
538 // In a legacy root, suspending without a boundary is always an error.
539 const uncaughtSuspenseError = new Error(
540 'A component suspended while responding to synchronous input. This ' +
541 'will cause the UI to be replaced with a loading indicator. To ' +
542 'fix, updates that suspend should be wrapped ' +
543 'with startTransition.',
544 );
545 value = uncaughtSuspenseError;
546 }
547 }
548 }
549 }
550
551 // This is a regular error, not a Suspense wakeable.
552 if (
553 getIsHydrating() &&
554 (disableLegacyMode || sourceFiber.mode & ConcurrentMode)
555 ) {
556 markDidThrowWhileHydratingDEV();
557 const hydrationBoundary = getSuspenseHandler();
558 // If the error was thrown during hydration, we may be able to recover by
559 // discarding the dehydrated content and switching to a client render.
560 // Instead of surfacing the error, find the nearest Suspense boundary
561 // and render it again without hydration.
562 if (hydrationBoundary !== null) {
563 if (__DEV__) {
564 if (hydrationBoundary.tag === SuspenseListComponent) {
565 console.error(
566 'SuspenseList should never catch while hydrating. This is a bug in React.',
567 );
568 }
569 }
570 if ((hydrationBoundary.flags & ShouldCapture) === NoFlags) {
571 // Set a flag to indicate that we should try rendering the normal
572 // children again, not the fallback.
573 hydrationBoundary.flags |= ForceClientRender;
574 }
575 markSuspenseBoundaryShouldCapture(
576 hydrationBoundary,
577 returnFiber,
578 sourceFiber,
579 root,
580 rootRenderLanes,
581 );
582
583 // Even though the user may not be affected by this error, we should
584 // still log it so it can be fixed.
585 if (value !== HydrationMismatchException) {
586 const wrapperError = new Error(
587 'There was an error while hydrating but React was able to recover by ' +
588 'instead client rendering from the nearest Suspense boundary.',
589 {cause: value},
590 );
591 queueHydrationError(
592 createCapturedValueAtFiber(wrapperError, sourceFiber),
593 );
594 }
595 return false;
596 } else {
597 if (value !== HydrationMismatchException) {
598 const wrapperError = new Error(
599 'There was an error while hydrating but React was able to recover by ' +
600 'instead client rendering the entire root.',
601 {cause: value},
602 );
603 queueHydrationError(
604 createCapturedValueAtFiber(wrapperError, sourceFiber),
605 );
606 }
607 const workInProgress: Fiber = (root.current as any).alternate;
608 // Schedule an update at the root to log the error but this shouldn't
609 // actually happen because we should recover.
610 workInProgress.flags |= ShouldCapture;
611 const lane = pickArbitraryLane(rootRenderLanes);
612 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
613 const rootErrorInfo = createCapturedValueAtFiber(value, sourceFiber);
614 const update = createRootErrorUpdate(
615 workInProgress.stateNode,
616 rootErrorInfo, // This should never actually get logged due to the recovery.
617 lane,
618 );
619 enqueueCapturedUpdate(workInProgress, update);
620 renderDidError();
621 return false;
622 }
623 } else {
624 // Otherwise, fall through to the error path.
625 }
626
627 const wrapperError = new Error(
628 'There was an error during concurrent rendering but React was able to recover by ' +
629 'instead synchronously rendering the entire root.',
630 {cause: value},
631 );
632 queueConcurrentError(createCapturedValueAtFiber(wrapperError, sourceFiber));
633 renderDidError();
634
635 // We didn't find a boundary that could handle this type of exception. Start
636 // over and traverse parent path again, this time treating the exception
637 // as an error.
638
639 if (returnFiber === null) {
640 // There's no return fiber, which means the root errored. This should never
641 // happen. Return `true` to trigger a fatal error (panic).
642 return true;
643 }
644
645 const errorInfo = createCapturedValueAtFiber(value, sourceFiber);
646 let workInProgress: Fiber = returnFiber;
647 do {
648 switch (workInProgress.tag) {
649 case HostRoot: {
650 workInProgress.flags |= ShouldCapture;
651 const lane = pickArbitraryLane(rootRenderLanes);
652 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
653 const update = createRootErrorUpdate(
654 workInProgress.stateNode,
655 errorInfo,
656 lane,
657 );
658 enqueueCapturedUpdate(workInProgress, update);
659 return false;
660 }
661 case ClassComponent:
662 // Capture and retry
663 const ctor = workInProgress.type;
664 const instance = workInProgress.stateNode;
665 if (
666 (workInProgress.flags & DidCapture) === NoFlags &&
667 (typeof ctor.getDerivedStateFromError === 'function' ||
668 (instance !== null &&
669 typeof instance.componentDidCatch === 'function' &&
670 !isAlreadyFailedLegacyErrorBoundary(instance)))
671 ) {
672 workInProgress.flags |= ShouldCapture;
673 const lane = pickArbitraryLane(rootRenderLanes);
674 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
675 // Schedule the error boundary to re-render using updated state
676 const update = createClassErrorUpdate(lane);
677 initializeClassErrorUpdate(update, root, workInProgress, errorInfo);
678 enqueueCapturedUpdate(workInProgress, update);
679 return false;
680 }
681 break;
682 case OffscreenComponent: {
683 const offscreenState: OffscreenState | null =
684 workInProgress.memoizedState as any;
685 if (offscreenState !== null) {
686 // An error was thrown inside a hidden Offscreen boundary. This should
687 // not be allowed to escape into the visible part of the UI. Mark the
688 // boundary with ShouldCapture to abort the ongoing prerendering
689 // attempt. This is the same flag would be set if something were to
690 // suspend. It will be cleared the next time the boundary
691 // is attempted.
692 workInProgress.flags |= ShouldCapture;
693 return false;
694 }
695 break;
696 }
697 default:
698 break;
699 }
700 // $FlowFixMe[incompatible-type] we bail out when we get a null
701 workInProgress = workInProgress.return;
702 } while (workInProgress !== null);
703
704 return false;
705 }
706
707 export {
708 throwException,
709 createRootErrorUpdate,
710 createClassErrorUpdate,
711 initializeClassErrorUpdate,
712 };