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 0220f988c15db..9dc38ab88a6d7 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';
@@ -1888,20 +1895,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;