Skip to content

Commit 52ca1a9

Browse files
committed
Create virtual Fiber when an error occurs during reconcilation
This lets us rethrow it in the conceptual place of the child.
1 parent a0a435d commit 52ca1a9

File tree

8 files changed

+109
-61
lines changed

8 files changed

+109
-61
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -964,67 +964,47 @@ describe('ReactFlight', () => {
964964
const testCases = (
965965
<>
966966
<ClientErrorBoundary expectedMessage="This is a real Error.">
967-
<div>
968-
<Throw value={new TypeError('This is a real Error.')} />
969-
</div>
967+
<Throw value={new TypeError('This is a real Error.')} />
970968
</ClientErrorBoundary>
971969
<ClientErrorBoundary expectedMessage="This is a string error.">
972-
<div>
973-
<Throw value="This is a string error." />
974-
</div>
970+
<Throw value="This is a string error." />
975971
</ClientErrorBoundary>
976972
<ClientErrorBoundary expectedMessage="{message: ..., extra: ..., nested: ...}">
977-
<div>
978-
<Throw
979-
value={{
980-
message: 'This is a long message',
981-
extra: 'properties',
982-
nested: {more: 'prop'},
983-
}}
984-
/>
985-
</div>
973+
<Throw
974+
value={{
975+
message: 'This is a long message',
976+
extra: 'properties',
977+
nested: {more: 'prop'},
978+
}}
979+
/>
986980
</ClientErrorBoundary>
987981
<ClientErrorBoundary
988982
expectedMessage={'{message: "Short", extra: ..., nested: ...}'}>
989-
<div>
990-
<Throw
991-
value={{
992-
message: 'Short',
993-
extra: 'properties',
994-
nested: {more: 'prop'},
995-
}}
996-
/>
997-
</div>
983+
<Throw
984+
value={{
985+
message: 'Short',
986+
extra: 'properties',
987+
nested: {more: 'prop'},
988+
}}
989+
/>
998990
</ClientErrorBoundary>
999991
<ClientErrorBoundary expectedMessage="Symbol(hello)">
1000-
<div>
1001-
<Throw value={Symbol('hello')} />
1002-
</div>
992+
<Throw value={Symbol('hello')} />
1003993
</ClientErrorBoundary>
1004994
<ClientErrorBoundary expectedMessage="123">
1005-
<div>
1006-
<Throw value={123} />
1007-
</div>
995+
<Throw value={123} />
1008996
</ClientErrorBoundary>
1009997
<ClientErrorBoundary expectedMessage="undefined">
1010-
<div>
1011-
<Throw value={undefined} />
1012-
</div>
998+
<Throw value={undefined} />
1013999
</ClientErrorBoundary>
10141000
<ClientErrorBoundary expectedMessage="<div/>">
1015-
<div>
1016-
<Throw value={<div />} />
1017-
</div>
1001+
<Throw value={<div />} />
10181002
</ClientErrorBoundary>
10191003
<ClientErrorBoundary expectedMessage="function Foo() {}">
1020-
<div>
1021-
<Throw value={function Foo() {}} />
1022-
</div>
1004+
<Throw value={function Foo() {}} />
10231005
</ClientErrorBoundary>
10241006
<ClientErrorBoundary expectedMessage={'["array"]'}>
1025-
<div>
1026-
<Throw value={['array']} />
1027-
</div>
1007+
<Throw value={['array']} />
10281008
</ClientErrorBoundary>
10291009
<ClientErrorBoundary
10301010
expectedMessage={
@@ -1034,9 +1014,7 @@ describe('ReactFlight', () => {
10341014
'- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
10351015
'- A compiler tries to "inline" JSX instead of using the runtime.'
10361016
}>
1037-
<div>
1038-
<LazyInlined />
1039-
</div>
1017+
<LazyInlined />
10401018
</ClientErrorBoundary>
10411019
</>
10421020
);

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function getInternalReactConstants(version: string): {
268268
TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't
269269
// want to fork again so we're adding it here instead
270270
YieldComponent: -1, // Removed
271+
Throw: 29,
271272
};
272273
} else if (gte(version, '17.0.0-alpha')) {
273274
ReactTypeOfWork = {
@@ -302,6 +303,7 @@ export function getInternalReactConstants(version: string): {
302303
SuspenseListComponent: 19, // Experimental
303304
TracingMarkerComponent: -1, // Doesn't exist yet
304305
YieldComponent: -1, // Removed
306+
Throw: -1, // Doesn't exist yet
305307
};
306308
} else if (gte(version, '16.6.0-beta.0')) {
307309
ReactTypeOfWork = {
@@ -336,6 +338,7 @@ export function getInternalReactConstants(version: string): {
336338
SuspenseListComponent: 19, // Experimental
337339
TracingMarkerComponent: -1, // Doesn't exist yet
338340
YieldComponent: -1, // Removed
341+
Throw: -1, // Doesn't exist yet
339342
};
340343
} else if (gte(version, '16.4.3-alpha')) {
341344
ReactTypeOfWork = {
@@ -370,6 +373,7 @@ export function getInternalReactConstants(version: string): {
370373
SuspenseListComponent: -1, // Doesn't exist yet
371374
TracingMarkerComponent: -1, // Doesn't exist yet
372375
YieldComponent: -1, // Removed
376+
Throw: -1, // Doesn't exist yet
373377
};
374378
} else {
375379
ReactTypeOfWork = {
@@ -404,6 +408,7 @@ export function getInternalReactConstants(version: string): {
404408
SuspenseListComponent: -1, // Doesn't exist yet
405409
TracingMarkerComponent: -1, // Doesn't exist yet
406410
YieldComponent: 9,
411+
Throw: -1, // Doesn't exist yet
407412
};
408413
}
409414
// **********************************************************
@@ -445,6 +450,7 @@ export function getInternalReactConstants(version: string): {
445450
SuspenseComponent,
446451
SuspenseListComponent,
447452
TracingMarkerComponent,
453+
Throw,
448454
} = ReactTypeOfWork;
449455

450456
function resolveFiberType(type: any): $FlowFixMe {
@@ -551,6 +557,9 @@ export function getInternalReactConstants(version: string): {
551557
return 'Profiler';
552558
case TracingMarkerComponent:
553559
return 'TracingMarker';
560+
case Throw:
561+
// This should really never be visible.
562+
return 'Error';
554563
default:
555564
const typeSymbol = getTypeSymbol(type);
556565

@@ -672,6 +681,7 @@ export function attach(
672681
SuspenseComponent,
673682
SuspenseListComponent,
674683
TracingMarkerComponent,
684+
Throw,
675685
} = ReactTypeOfWork;
676686
const {
677687
ImmediatePriority,
@@ -1036,6 +1046,7 @@ export function attach(
10361046
case HostText:
10371047
case LegacyHiddenComponent:
10381048
case OffscreenComponent:
1049+
case Throw:
10391050
return true;
10401051
case HostRoot:
10411052
// It is never valid to filter the root element.

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type WorkTagMap = {
7272
SuspenseListComponent: WorkTag,
7373
TracingMarkerComponent: WorkTag,
7474
YieldComponent: WorkTag,
75+
Throw: WorkTag,
7576
};
7677

7778
// TODO: If it's useful for the frontend to know which types of data an Element has

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
Forked,
2626
PlacementDEV,
2727
} from './ReactFiberFlags';
28+
import {NoMode, ConcurrentMode} from './ReactTypeOfMode';
2829
import {
2930
getIteratorFn,
3031
ASYNC_ITERATOR,
@@ -46,6 +47,7 @@ import isArray from 'shared/isArray';
4647
import {
4748
enableRefAsProp,
4849
enableAsyncIterableChildren,
50+
disableLegacyMode,
4951
} from 'shared/ReactFeatureFlags';
5052

5153
import {
@@ -55,11 +57,16 @@ import {
5557
createFiberFromFragment,
5658
createFiberFromText,
5759
createFiberFromPortal,
60+
createFiberFromThrow,
5861
} from './ReactFiber';
5962
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
6063
import {getIsHydrating} from './ReactFiberHydrationContext';
6164
import {pushTreeFork} from './ReactFiberTreeContext';
62-
import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
65+
import {
66+
SuspenseException,
67+
createThenableState,
68+
trackUsedThenable,
69+
} from './ReactFiberThenable';
6370
import {readContextDuringReconciliation} from './ReactFiberNewContext';
6471
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
6572

@@ -1919,20 +1926,45 @@ function createChildReconciler(
19191926
newChild: any,
19201927
lanes: Lanes,
19211928
): Fiber | null {
1922-
// This indirection only exists so we can reset `thenableState` at the end.
1923-
// It should get inlined by Closure.
1924-
thenableIndexCounter = 0;
1925-
const firstChildFiber = reconcileChildFibersImpl(
1926-
returnFiber,
1927-
currentFirstChild,
1928-
newChild,
1929-
lanes,
1930-
null, // debugInfo
1931-
);
1932-
thenableState = null;
1933-
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1934-
// set at the beginning.
1935-
return firstChildFiber;
1929+
try {
1930+
// This indirection only exists so we can reset `thenableState` at the end.
1931+
// It should get inlined by Closure.
1932+
thenableIndexCounter = 0;
1933+
const firstChildFiber = reconcileChildFibersImpl(
1934+
returnFiber,
1935+
currentFirstChild,
1936+
newChild,
1937+
lanes,
1938+
null, // debugInfo
1939+
);
1940+
thenableState = null;
1941+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1942+
// set at the beginning.
1943+
return firstChildFiber;
1944+
} catch (x) {
1945+
if (
1946+
x === SuspenseException ||
1947+
(!disableLegacyMode &&
1948+
(returnFiber.mode & ConcurrentMode) === NoMode &&
1949+
typeof x === 'object' &&
1950+
x !== null &&
1951+
typeof x.then === 'function')
1952+
) {
1953+
// Suspense exceptions need to read the current suspended state before
1954+
// yielding and replay it using the same sequence so this trick doesn't
1955+
// work here.
1956+
// Suspending in legacy mode actually mounts so if we let the child
1957+
// mount then we delete its state in an update.
1958+
throw x;
1959+
}
1960+
// Something errored during reconciliation but it's conceptually a child that
1961+
// errored and not the current component itself so we create a virtual child
1962+
// that throws in its begin phase. That way the current component can handle
1963+
// the error or suspending if needed.
1964+
const throwFiber = createFiberFromThrow(x, returnFiber.mode, lanes);
1965+
throwFiber.return = returnFiber;
1966+
return throwFiber;
1967+
}
19361968
}
19371969

19381970
return reconcileChildFibers;

packages/react-reconciler/src/ReactFiber.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
OffscreenComponent,
6868
LegacyHiddenComponent,
6969
TracingMarkerComponent,
70+
Throw,
7071
} from './ReactWorkTags';
7172
import {OffscreenVisible} from './ReactFiberActivityComponent';
7273
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
@@ -879,3 +880,13 @@ export function createFiberFromPortal(
879880
};
880881
return fiber;
881882
}
883+
884+
export function createFiberFromThrow(
885+
error: mixed,
886+
mode: TypeOfMode,
887+
lanes: Lanes,
888+
): Fiber {
889+
const fiber = createFiber(Throw, error, null, mode);
890+
fiber.lanes = lanes;
891+
return fiber;
892+
}

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
LegacyHiddenComponent,
7373
CacheComponent,
7474
TracingMarkerComponent,
75+
Throw,
7576
} from './ReactWorkTags';
7677
import {
7778
NoFlags,
@@ -4126,6 +4127,11 @@ function beginWork(
41264127
}
41274128
break;
41284129
}
4130+
case Throw: {
4131+
// This represents a Component that threw in the reconciliation phase.
4132+
// So we'll rethrow here. This might be
4133+
throw workInProgress.pendingProps;
4134+
}
41294135
}
41304136

41314137
throw new Error(

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
LegacyHiddenComponent,
7373
CacheComponent,
7474
TracingMarkerComponent,
75+
Throw,
7576
} from './ReactWorkTags';
7677
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
7778
import {
@@ -1802,6 +1803,12 @@ function completeWork(
18021803
}
18031804
return null;
18041805
}
1806+
case Throw: {
1807+
if (!disableLegacyMode) {
1808+
// Only Legacy Mode completes an errored node.
1809+
return null;
1810+
}
1811+
}
18051812
}
18061813

18071814
throw new Error(

packages/react-reconciler/src/ReactWorkTags.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export type WorkTag =
3636
| 25
3737
| 26
3838
| 27
39-
| 28;
39+
| 28
40+
| 29;
4041

4142
export const FunctionComponent = 0;
4243
export const ClassComponent = 1;
@@ -65,3 +66,4 @@ export const TracingMarkerComponent = 25;
6566
export const HostHoistable = 26;
6667
export const HostSingleton = 27;
6768
export const IncompleteFunctionComponent = 28;
69+
export const Throw = 29;

0 commit comments

Comments
 (0)