From 52ca1a9db58eb3c498f63a4d3b31eb67299178f2 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 7 Jun 2024 12:36:55 -0400 Subject: [PATCH] Create virtual Fiber when an error occurs during reconcilation This lets us rethrow it in the conceptual place of the child. --- .../src/__tests__/ReactFlight-test.js | 68 +++++++------------ .../src/backend/renderer.js | 11 +++ .../src/backend/types.js | 1 + .../react-reconciler/src/ReactChildFiber.js | 62 +++++++++++++---- packages/react-reconciler/src/ReactFiber.js | 11 +++ .../src/ReactFiberBeginWork.js | 6 ++ .../src/ReactFiberCompleteWork.js | 7 ++ .../react-reconciler/src/ReactWorkTags.js | 4 +- 8 files changed, 109 insertions(+), 61 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index bc660a207f075..77fb121f64dc5 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -964,67 +964,47 @@ describe('ReactFlight', () => { const testCases = ( <> -
- -
+
-
- -
+
-
- -
+
-
- -
+
-
- -
+
-
- -
+
-
- -
+
-
- } /> -
+ } />
-
- -
+
-
- -
+
{ '- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' + '- A compiler tries to "inline" JSX instead of using the runtime.' }> -
- -
+
); diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 2e6cc9b287812..666493cbd9a3d 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -268,6 +268,7 @@ export function getInternalReactConstants(version: string): { TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't // want to fork again so we're adding it here instead YieldComponent: -1, // Removed + Throw: 29, }; } else if (gte(version, '17.0.0-alpha')) { ReactTypeOfWork = { @@ -302,6 +303,7 @@ export function getInternalReactConstants(version: string): { SuspenseListComponent: 19, // Experimental TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet }; } else if (gte(version, '16.6.0-beta.0')) { ReactTypeOfWork = { @@ -336,6 +338,7 @@ export function getInternalReactConstants(version: string): { SuspenseListComponent: 19, // Experimental TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet }; } else if (gte(version, '16.4.3-alpha')) { ReactTypeOfWork = { @@ -370,6 +373,7 @@ export function getInternalReactConstants(version: string): { SuspenseListComponent: -1, // Doesn't exist yet TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet }; } else { ReactTypeOfWork = { @@ -404,6 +408,7 @@ export function getInternalReactConstants(version: string): { SuspenseListComponent: -1, // Doesn't exist yet TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: 9, + Throw: -1, // Doesn't exist yet }; } // ********************************************************** @@ -445,6 +450,7 @@ export function getInternalReactConstants(version: string): { SuspenseComponent, SuspenseListComponent, TracingMarkerComponent, + Throw, } = ReactTypeOfWork; function resolveFiberType(type: any): $FlowFixMe { @@ -551,6 +557,9 @@ export function getInternalReactConstants(version: string): { return 'Profiler'; case TracingMarkerComponent: return 'TracingMarker'; + case Throw: + // This should really never be visible. + return 'Error'; default: const typeSymbol = getTypeSymbol(type); @@ -672,6 +681,7 @@ export function attach( SuspenseComponent, SuspenseListComponent, TracingMarkerComponent, + Throw, } = ReactTypeOfWork; const { ImmediatePriority, @@ -1036,6 +1046,7 @@ export function attach( case HostText: case LegacyHiddenComponent: case OffscreenComponent: + case Throw: return true; case HostRoot: // It is never valid to filter the root element. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index f11f631cd9621..a64d1e5337f34 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -72,6 +72,7 @@ export type WorkTagMap = { SuspenseListComponent: WorkTag, TracingMarkerComponent: WorkTag, YieldComponent: WorkTag, + Throw: WorkTag, }; // TODO: If it's useful for the frontend to know which types of data an Element has diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 9334107301b0c..60f8dd97468ba 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -25,6 +25,7 @@ import { Forked, PlacementDEV, } from './ReactFiberFlags'; +import {NoMode, ConcurrentMode} from './ReactTypeOfMode'; import { getIteratorFn, ASYNC_ITERATOR, @@ -46,6 +47,7 @@ import isArray from 'shared/isArray'; import { enableRefAsProp, enableAsyncIterableChildren, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import { @@ -55,11 +57,16 @@ import { createFiberFromFragment, createFiberFromText, createFiberFromPortal, + createFiberFromThrow, } from './ReactFiber'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading'; import {getIsHydrating} from './ReactFiberHydrationContext'; import {pushTreeFork} from './ReactFiberTreeContext'; -import {createThenableState, trackUsedThenable} from './ReactFiberThenable'; +import { + SuspenseException, + createThenableState, + trackUsedThenable, +} from './ReactFiberThenable'; import {readContextDuringReconciliation} from './ReactFiberNewContext'; import {callLazyInitInDEV} from './ReactFiberCallUserSpace'; @@ -1919,20 +1926,45 @@ function createChildReconciler( newChild: any, lanes: Lanes, ): Fiber | null { - // This indirection only exists so we can reset `thenableState` at the end. - // It should get inlined by Closure. - thenableIndexCounter = 0; - const firstChildFiber = reconcileChildFibersImpl( - returnFiber, - currentFirstChild, - newChild, - lanes, - null, // debugInfo - ); - thenableState = null; - // Don't bother to reset `thenableIndexCounter` to 0 because it always gets - // set at the beginning. - return firstChildFiber; + try { + // This indirection only exists so we can reset `thenableState` at the end. + // It should get inlined by Closure. + thenableIndexCounter = 0; + const firstChildFiber = reconcileChildFibersImpl( + returnFiber, + currentFirstChild, + newChild, + lanes, + null, // debugInfo + ); + thenableState = null; + // Don't bother to reset `thenableIndexCounter` to 0 because it always gets + // set at the beginning. + return firstChildFiber; + } catch (x) { + if ( + x === SuspenseException || + (!disableLegacyMode && + (returnFiber.mode & ConcurrentMode) === NoMode && + typeof x === 'object' && + x !== null && + typeof x.then === 'function') + ) { + // Suspense exceptions need to read the current suspended state before + // yielding and replay it using the same sequence so this trick doesn't + // work here. + // Suspending in legacy mode actually mounts so if we let the child + // mount then we delete its state in an update. + throw x; + } + // Something errored during reconciliation but it's conceptually a child that + // errored and not the current component itself so we create a virtual child + // that throws in its begin phase. That way the current component can handle + // the error or suspending if needed. + const throwFiber = createFiberFromThrow(x, returnFiber.mode, lanes); + throwFiber.return = returnFiber; + return throwFiber; + } } return reconcileChildFibers; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 7234117a50c7f..28cd65dbb3c07 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -67,6 +67,7 @@ import { OffscreenComponent, LegacyHiddenComponent, TracingMarkerComponent, + Throw, } from './ReactWorkTags'; import {OffscreenVisible} from './ReactFiberActivityComponent'; import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber'; @@ -879,3 +880,13 @@ export function createFiberFromPortal( }; return fiber; } + +export function createFiberFromThrow( + error: mixed, + mode: TypeOfMode, + lanes: Lanes, +): Fiber { + const fiber = createFiber(Throw, error, null, mode); + fiber.lanes = lanes; + return fiber; +} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 793a3fa942682..8a03e53f30a6f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -72,6 +72,7 @@ import { LegacyHiddenComponent, CacheComponent, TracingMarkerComponent, + Throw, } from './ReactWorkTags'; import { NoFlags, @@ -4126,6 +4127,11 @@ function beginWork( } break; } + case Throw: { + // This represents a Component that threw in the reconciliation phase. + // So we'll rethrow here. This might be + throw workInProgress.pendingProps; + } } throw new Error( diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 4a671940ba1ac..dd797b8d09717 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -72,6 +72,7 @@ import { LegacyHiddenComponent, CacheComponent, TracingMarkerComponent, + Throw, } from './ReactWorkTags'; import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; import { @@ -1802,6 +1803,12 @@ function completeWork( } return null; } + case Throw: { + if (!disableLegacyMode) { + // Only Legacy Mode completes an errored node. + return null; + } + } } throw new Error( diff --git a/packages/react-reconciler/src/ReactWorkTags.js b/packages/react-reconciler/src/ReactWorkTags.js index a4d4eb1751548..0af97479ef833 100644 --- a/packages/react-reconciler/src/ReactWorkTags.js +++ b/packages/react-reconciler/src/ReactWorkTags.js @@ -36,7 +36,8 @@ export type WorkTag = | 25 | 26 | 27 - | 28; + | 28 + | 29; export const FunctionComponent = 0; export const ClassComponent = 1; @@ -65,3 +66,4 @@ export const TracingMarkerComponent = 25; export const HostHoistable = 26; export const HostSingleton = 27; export const IncompleteFunctionComponent = 28; +export const Throw = 29;