Skip to content

Commit 613f75f

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 b686f22 commit 613f75f

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
@@ -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,
@@ -648,7 +649,10 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
648649
// Check if the render expired.
649650
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
650651
if (didTimeout) {
651-
if (workInProgressRoot === root && renderExpirationTime !== NoWork) {
652+
if (
653+
root === workInProgressRoot &&
654+
expirationTime === renderExpirationTime
655+
) {
652656
// We're already in the middle of working on this tree. Expire the tree at
653657
// the already-rendering time so we don't throw out the partial work. If
654658
// there's another expired level after that, we'll hit the `else` branch
@@ -777,6 +781,7 @@ function finishConcurrentRender(
777781
root,
778782
expirationTime > Idle ? Idle : expirationTime,
779783
);
784+
root.didError = true;
780785
// We assume that this second render pass will be synchronous
781786
// and therefore not hit this path again.
782787
break;
@@ -1057,7 +1062,7 @@ function performSyncWorkOnRoot(root) {
10571062
stopFinishedWorkLoopTimer();
10581063
root.finishedWork = (root.current.alternate: any);
10591064
root.finishedExpirationTime = expirationTime;
1060-
finishSyncRender(root);
1065+
finishSyncRender(root, workInProgressRootExitStatus);
10611066
}
10621067

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

10771103
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)