Skip to content

Commit 51ef007

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 4f22777 commit 51ef007

File tree

4 files changed

+62
-6
lines changed

4 files changed

+62
-6
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: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
SimpleMemoComponent,
9090
Chunk,
9191
} from 'shared/ReactWorkTags';
92+
import {LegacyRoot} from 'shared/ReactRootTags';
9293
import {
9394
NoEffect,
9495
PerformedWork,
@@ -647,7 +648,10 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
647648
// Check if the render expired.
648649
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
649650
if (didTimeout) {
650-
if (workInProgressRoot === root && renderExpirationTime !== NoWork) {
651+
if (
652+
root === workInProgressRoot &&
653+
expirationTime === renderExpirationTime
654+
) {
651655
// We're already in the middle of working on this tree. Expire the tree at
652656
// the already-rendering time so we don't throw out the partial work. If
653657
// there's another expired level after that, we'll hit the `else` branch
@@ -776,6 +780,7 @@ function finishConcurrentRender(
776780
root,
777781
expirationTime > Idle ? Idle : expirationTime,
778782
);
783+
root.didError = true;
779784
// We assume that this second render pass will be synchronous
780785
// and therefore not hit this path again.
781786
break;
@@ -1078,7 +1083,25 @@ function finishSyncRender(root, exitStatus, expirationTime) {
10781083
flushSuspensePriorityWarningInDEV();
10791084
}
10801085
}
1081-
commitRoot(root);
1086+
1087+
// Try rendering one more time, synchronously, to see if the error goes away.
1088+
// If there are lower priority updates, let's include those, too, in case they
1089+
// fix the inconsistency. Render at Idle to include all updates. If it was
1090+
// Idle or Never or some not-yet-invented time, render at that time.
1091+
if (
1092+
workInProgressRootExitStatus === RootErrored &&
1093+
// Legacy mode shouldn't retry, only blocking and concurrent mode
1094+
root.tag !== LegacyRoot &&
1095+
// Don't retry more than once.
1096+
!root.didError
1097+
) {
1098+
root.didError = true;
1099+
markRootExpiredAtTime(root, expirationTime > Idle ? Idle : expirationTime);
1100+
} else {
1101+
// In all other cases, since this was a synchronous render,
1102+
// commit immediately.
1103+
commitRoot(root);
1104+
}
10821105
}
10831106

10841107
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
@@ -588,7 +588,12 @@ describe('ReactIncrementalErrorHandling', () => {
588588
expect(ops).toEqual([
589589
'ErrorBoundary render success',
590590
'BrokenRender',
591-
// React doesn't retry because we're already rendering synchronously.
591+
592+
// React retries one more time
593+
'ErrorBoundary render success',
594+
'BrokenRender',
595+
596+
// Errored again on retry. Now handle it.
592597
'ErrorBoundary componentDidCatch',
593598
'ErrorBoundary render error',
594599
]);
@@ -632,7 +637,12 @@ describe('ReactIncrementalErrorHandling', () => {
632637
expect(ops).toEqual([
633638
'ErrorBoundary render success',
634639
'BrokenRender',
635-
// React doesn't retry because we're already rendering synchronously.
640+
641+
// React retries one more time
642+
'ErrorBoundary render success',
643+
'BrokenRender',
644+
645+
// Errored again on retry. Now handle it.
636646
'ErrorBoundary componentDidCatch',
637647
'ErrorBoundary render error',
638648
]);
@@ -751,7 +761,12 @@ describe('ReactIncrementalErrorHandling', () => {
751761
expect(ops).toEqual([
752762
'RethrowErrorBoundary render',
753763
'BrokenRender',
754-
// React doesn't retry because we're already rendering synchronously.
764+
765+
// React retries one more time
766+
'RethrowErrorBoundary render',
767+
'BrokenRender',
768+
769+
// Errored again on retry. Now handle it.
755770
'RethrowErrorBoundary componentDidCatch',
756771
]);
757772
expect(ReactNoop.getChildren()).toEqual([]);
@@ -790,7 +805,12 @@ describe('ReactIncrementalErrorHandling', () => {
790805
expect(ops).toEqual([
791806
'RethrowErrorBoundary render',
792807
'BrokenRender',
793-
// React doesn't retry because we're already rendering synchronously.
808+
809+
// React retries one more time
810+
'RethrowErrorBoundary render',
811+
'BrokenRender',
812+
813+
// Errored again on retry. Now handle it.
794814
'RethrowErrorBoundary componentDidCatch',
795815
]);
796816
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)