Skip to content

Commit b57e228

Browse files
committed
Retry once after error even for sync renders
Except in legacy mode. This is to support the `useOpaqueReference` hook, which uses an error to trigger a retry at lower priority.
1 parent f176a00 commit b57e228

File tree

4 files changed

+66
-7
lines changed

4 files changed

+66
-7
lines changed

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ type BaseFiberRootProperties = {|
7474
// render again
7575
lastPingedTime: ExpirationTime,
7676
lastExpiredTime: ExpirationTime,
77+
78+
// Set to true when the root completes with an error. We'll try rendering
79+
// again. This field ensures we don't retry more than once.
80+
didError: boolean,
7781
|};
7882

7983
// The following attributes are only used by interaction tracing builds.
@@ -123,6 +127,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
123127
this.nextKnownPendingLevel = NoWork;
124128
this.lastPingedTime = NoWork;
125129
this.lastExpiredTime = NoWork;
130+
this.didError = false;
126131

127132
if (enableSchedulerTracing) {
128133
this.interactionThreadID = unstable_getThreadID();
@@ -249,6 +254,8 @@ export function markRootFinishedAtTime(
249254
// Clear the expired time
250255
root.lastExpiredTime = NoWork;
251256
}
257+
258+
root.didError = false;
252259
}
253260

254261
export function markRootExpiredAtTime(

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
SimpleMemoComponent,
9191
Block,
9292
} from 'shared/ReactWorkTags';
93+
import {LegacyRoot} from 'shared/ReactRootTags';
9394
import {
9495
NoEffect,
9596
PerformedWork,
@@ -646,7 +647,10 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
646647
// Check if the render expired.
647648
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
648649
if (didTimeout) {
649-
if (workInProgressRoot === root && renderExpirationTime !== NoWork) {
650+
if (
651+
root === workInProgressRoot &&
652+
expirationTime === renderExpirationTime
653+
) {
650654
// We're already in the middle of working on this tree. Expire the tree at
651655
// the already-rendering time so we don't throw out the partial work. If
652656
// there's another expired level after that, we'll hit the `else` branch
@@ -775,6 +779,7 @@ function finishConcurrentRender(
775779
root,
776780
expirationTime > Idle ? Idle : expirationTime,
777781
);
782+
root.didError = true;
778783
// We assume that this second render pass will be synchronous
779784
// and therefore not hit this path again.
780785
break;
@@ -1055,7 +1060,7 @@ function performSyncWorkOnRoot(root) {
10551060
stopFinishedWorkLoopTimer();
10561061
root.finishedWork = (root.current.alternate: any);
10571062
root.finishedExpirationTime = expirationTime;
1058-
finishSyncRender(root);
1063+
finishSyncRender(root, workInProgressRootExitStatus);
10591064
}
10601065

10611066
// Before exiting, make sure there's a callback scheduled for the next
@@ -1069,7 +1074,28 @@ function performSyncWorkOnRoot(root) {
10691074
function finishSyncRender(root) {
10701075
// Set this to null to indicate there's no in-progress render.
10711076
workInProgressRoot = null;
1072-
commitRoot(root);
1077+
1078+
// Try rendering one more time, synchronously, to see if the error goes away.
1079+
// If there are lower priority updates, let's include those, too, in case they
1080+
// fix the inconsistency. Render at Idle to include all updates. If it was
1081+
// Idle or Never or some not-yet-invented time, render at that time.
1082+
if (
1083+
workInProgressRootExitStatus === RootErrored &&
1084+
// Legacy mode shouldn't retry, only blocking and concurrent mode
1085+
root.tag !== LegacyRoot &&
1086+
// Don't retry more than once.
1087+
!root.didError
1088+
) {
1089+
root.didError = true;
1090+
markRootExpiredAtTime(
1091+
root,
1092+
renderExpirationTime > Idle ? Idle : renderExpirationTime,
1093+
);
1094+
} else {
1095+
// In all other cases, since this was a synchronous render,
1096+
// commit immediately.
1097+
commitRoot(root);
1098+
}
10731099
}
10741100

10751101
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {

packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,12 @@ describe('ReactIncrementalErrorHandling', () => {
589589
expect(ops).toEqual([
590590
'ErrorBoundary render success',
591591
'BrokenRender',
592-
// React doesn't retry because we're already rendering synchronously.
592+
593+
// React retries one more time
594+
'ErrorBoundary render success',
595+
'BrokenRender',
596+
597+
// Errored again on retry. Now handle it.
593598
'ErrorBoundary componentDidCatch',
594599
'ErrorBoundary render error',
595600
]);
@@ -633,7 +638,12 @@ describe('ReactIncrementalErrorHandling', () => {
633638
expect(ops).toEqual([
634639
'ErrorBoundary render success',
635640
'BrokenRender',
636-
// React doesn't retry because we're already rendering synchronously.
641+
642+
// React retries one more time
643+
'ErrorBoundary render success',
644+
'BrokenRender',
645+
646+
// Errored again on retry. Now handle it.
637647
'ErrorBoundary componentDidCatch',
638648
'ErrorBoundary render error',
639649
]);
@@ -752,7 +762,12 @@ describe('ReactIncrementalErrorHandling', () => {
752762
expect(ops).toEqual([
753763
'RethrowErrorBoundary render',
754764
'BrokenRender',
755-
// React doesn't retry because we're already rendering synchronously.
765+
766+
// React retries one more time
767+
'RethrowErrorBoundary render',
768+
'BrokenRender',
769+
770+
// Errored again on retry. Now handle it.
756771
'RethrowErrorBoundary componentDidCatch',
757772
]);
758773
expect(ReactNoop.getChildren()).toEqual([]);
@@ -791,7 +806,12 @@ describe('ReactIncrementalErrorHandling', () => {
791806
expect(ops).toEqual([
792807
'RethrowErrorBoundary render',
793808
'BrokenRender',
794-
// React doesn't retry because we're already rendering synchronously.
809+
810+
// React retries one more time
811+
'RethrowErrorBoundary render',
812+
'BrokenRender',
813+
814+
// Errored again on retry. Now handle it.
795815
'RethrowErrorBoundary componentDidCatch',
796816
]);
797817
expect(ReactNoop.getChildren()).toEqual([]);

packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,14 @@ describe('ReactIncrementalErrorLogging', () => {
189189
[
190190
'render: 0',
191191
__DEV__ && 'render: 0', // replay
192+
192193
'render: 1',
193194
__DEV__ && 'render: 1', // replay
195+
196+
// Retry one more time before handling error
197+
'render: 1',
198+
__DEV__ && 'render: 1', // replay
199+
194200
'componentWillUnmount: 0',
195201
].filter(Boolean),
196202
);

0 commit comments

Comments
 (0)