diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
index 1d5f5226e7d5b..1b3283a730310 100644
--- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
@@ -752,7 +752,17 @@ describe('ReactInternalTestUtils console assertions', () => {
You must call one of the assertConsoleDev helpers between each act call."
`);
- await waitForAll(['A', 'B', 'A', 'B']);
+ await waitForAll([
+ 'A',
+ 'B',
+ ...(gate(flags => flags.enableUnifiedSyncLane)
+ ? []
+ : [
+ // React will try one more time in non-blocking updates before giving up.
+ 'A',
+ 'B',
+ ]),
+ ]);
});
test('should fail if waitForPaint is called before asserting', async () => {
@@ -1684,7 +1694,17 @@ describe('ReactInternalTestUtils console assertions', () => {
You must call one of the assertConsoleDev helpers between each act call."
`);
- await waitForAll(['A', 'B', 'A', 'B']);
+ await waitForAll([
+ 'A',
+ 'B',
+ ...(gate(flags => flags.enableUnifiedSyncLane)
+ ? []
+ : [
+ // React will try one more time in non-blocking updates before giving up.
+ 'A',
+ 'B',
+ ]),
+ ]);
});
test('should fail if waitForPaint is called before asserting', async () => {
@@ -2634,7 +2654,17 @@ describe('ReactInternalTestUtils console assertions', () => {
You must call one of the assertConsoleDev helpers between each act call."
`);
- await waitForAll(['A', 'B', 'A', 'B']);
+ await waitForAll([
+ 'A',
+ 'B',
+ ...(gate(flags => flags.enableUnifiedSyncLane)
+ ? []
+ : [
+ // React will try one more time in non-blocking updates before giving up.
+ 'A',
+ 'B',
+ ]),
+ ]);
});
test('should fail if waitForPaint is called before asserting', async () => {
diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
index c8d6e3ceae60b..59a59b38d65a9 100644
--- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
+++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
@@ -2150,8 +2150,6 @@ describe('Timeline profiler', () => {
);
await waitForAll([
- 'ErrorBoundary render',
- 'ExampleThatThrows',
'ErrorBoundary render',
'ExampleThatThrows',
'ErrorBoundary fallback',
@@ -2159,81 +2157,60 @@ describe('Timeline profiler', () => {
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.componentMeasures).toMatchInlineSnapshot(`
- [
- {
- "componentName": "ErrorBoundary",
- "duration": 10,
- "timestamp": 10,
- "type": "render",
- "warning": null,
- },
- {
- "componentName": "ExampleThatThrows",
- "duration": 0,
- "timestamp": 20,
- "type": "render",
- "warning": null,
- },
- {
- "componentName": "ErrorBoundary",
- "duration": 10,
- "timestamp": 20,
- "type": "render",
- "warning": null,
- },
- {
- "componentName": "ExampleThatThrows",
- "duration": 0,
- "timestamp": 30,
- "type": "render",
- "warning": null,
- },
- {
- "componentName": "ErrorBoundary",
- "duration": 10,
- "timestamp": 30,
- "type": "render",
- "warning": null,
- },
- ]
- `);
+ [
+ {
+ "componentName": "ErrorBoundary",
+ "duration": 10,
+ "timestamp": 10,
+ "type": "render",
+ "warning": null,
+ },
+ {
+ "componentName": "ExampleThatThrows",
+ "duration": 0,
+ "timestamp": 20,
+ "type": "render",
+ "warning": null,
+ },
+ {
+ "componentName": "ErrorBoundary",
+ "duration": 10,
+ "timestamp": 20,
+ "type": "render",
+ "warning": null,
+ },
+ ]
+ `);
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
- [
- {
- "lanes": "0b0000000000000000000000000100000",
- "timestamp": 10,
- "type": "schedule-render",
- "warning": null,
- },
- {
- "componentName": "ErrorBoundary",
- "componentStack": "
- in ErrorBoundary (at **)",
- "lanes": "0b0000000000000000000000000000010",
- "timestamp": 30,
- "type": "schedule-state-update",
- "warning": null,
- },
- ]
- `);
+ [
+ {
+ "lanes": "0b0000000000000000000000000100000",
+ "timestamp": 10,
+ "type": "schedule-render",
+ "warning": null,
+ },
+ {
+ "componentName": "ErrorBoundary",
+ "componentStack": "
+ in ErrorBoundary (at **)",
+ "lanes": "0b0000000000000000000000000000010",
+ "timestamp": 20,
+ "type": "schedule-state-update",
+ "warning": null,
+ },
+ ]
+ `);
expect(timelineData.thrownErrors).toMatchInlineSnapshot(`
- [
- {
- "componentName": "ExampleThatThrows",
- "message": "Expected error",
- "phase": "mount",
- "timestamp": 20,
- "type": "thrown-error",
- },
- {
- "componentName": "ExampleThatThrows",
- "message": "Expected error",
- "phase": "mount",
- "timestamp": 30,
- "type": "thrown-error",
- },
- ]
- `);
+ [
+ {
+ "componentName": "ExampleThatThrows",
+ "message": "Expected error",
+ "phase": "mount",
+ "timestamp": 20,
+ "type": "thrown-error",
+ },
+ ]
+ `);
});
it('should mark passive and layout effects', async () => {
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 2592807cde69a..9de3f1c188405 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -1364,14 +1364,13 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
}
let exitStatus = renderRootSync(root, lanes);
+ const wasRootDehydrated = supportsHydration && isRootDehydrated(root);
if (
(disableLegacyMode || root.tag !== LegacyRoot) &&
- exitStatus === RootErrored
+ exitStatus === RootErrored &&
+ wasRootDehydrated
) {
- // If something threw an error, try rendering one more time. We'll render
- // synchronously to block concurrent data mutations, and we'll includes
- // all pending updates are included. If it still fails after the second
- // attempt, we'll give up and commit the resulting tree.
+ // If something threw an error and we have work to do, try rendering one more time.
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index 834bbeb0e7d02..b5dd6c37b937c 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -380,7 +380,18 @@ describe('ReactIncrementalErrorHandling', () => {
// Render the bad component synchronously
ReactNoop.render(, () => Scheduler.log('commit'));
- await waitFor(['Parent', 'BadRender', 'commit']);
+ await waitFor([
+ 'Parent',
+ 'BadRender',
+ ...(gate(flags => flags.enableUnifiedSyncLane)
+ ? []
+ : [
+ // Retry in non-blocking updates
+ 'Parent',
+ 'BadRender',
+ ]),
+ 'commit',
+ ]);
expect(ReactNoop).toMatchRenderedOutput(null);
});
@@ -410,7 +421,7 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop.render(, () => Scheduler.log('commit'));
});
- assertLog(['Parent', 'BadRender', 'Parent', 'BadRender', 'commit']);
+ assertLog(['Parent', 'BadRender', 'commit']);
expect(ReactNoop).toMatchRenderedOutput(null);
});
@@ -666,12 +677,6 @@ describe('ReactIncrementalErrorHandling', () => {
assertLog([
'ErrorBoundary render success',
'BrokenRender',
-
- // React retries one more time
- 'ErrorBoundary render success',
- 'BrokenRender',
-
- // Errored again on retry. Now handle it.
'ErrorBoundary componentDidCatch',
'ErrorBoundary render error',
]);
@@ -716,12 +721,6 @@ describe('ReactIncrementalErrorHandling', () => {
assertLog([
'ErrorBoundary render success',
'BrokenRender',
-
- // React retries one more time
- 'ErrorBoundary render success',
- 'BrokenRender',
-
- // Errored again on retry. Now handle it.
'ErrorBoundary componentDidCatch',
'ErrorBoundary render error',
]);
@@ -840,12 +839,6 @@ describe('ReactIncrementalErrorHandling', () => {
assertLog([
'RethrowErrorBoundary render',
'BrokenRender',
-
- // React retries one more time
- 'RethrowErrorBoundary render',
- 'BrokenRender',
-
- // Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -883,10 +876,6 @@ describe('ReactIncrementalErrorHandling', () => {
'RethrowErrorBoundary render',
'BrokenRender',
- // React retries one more time
- 'RethrowErrorBoundary render',
- 'BrokenRender',
-
// Errored again on retry. Now handle it.
'RethrowErrorBoundary componentDidCatch',
]);
@@ -1892,27 +1881,31 @@ describe('ReactIncrementalErrorHandling', () => {
root.render();
});
- await act(async () => {
- // Schedule a default pri and a low pri update on the root.
- root.render();
- React.startTransition(() => {
- root.render();
- });
-
- // Render through just the default pri update. The low pri update remains on
- // the queue.
- await waitFor(['Everything is fine.']);
+ await expect(
+ act(async () => {
+ // Schedule a default pri and a low pri update on the root.
+ root.render();
+ React.startTransition(() => {
+ root.render();
+ });
- // Schedule a discrete update on a child that triggers an error.
- // The root should capture this error. But since there's still a pending
- // update on the root, the error should be suppressed.
- ReactNoop.discreteUpdates(() => {
- setShouldThrow(true);
- });
- });
- // Should render the final state without throwing the error.
- assertLog(['Everything is fine.']);
- expect(root).toMatchRenderedOutput('Everything is fine.');
+ // Render through the default pri and low pri update.
+ await waitFor(['Everything is fine.']);
+
+ // Schedule a discrete update on a child that triggers an error.
+ // Schedule a discrete update on a child that triggers an error.
+ // The root should capture this error. But since there's still a pending
+ // update on the root, the error should be suppressed.
+ // Schedule a discrete update on a child that triggers an error.
+ // The root should capture this error. But since there's still a pending
+ // update on the root, the error should be suppressed.
+ ReactNoop.discreteUpdates(() => {
+ setShouldThrow(true);
+ });
+ }),
+ ).rejects.toThrow('Oops');
+ assertLog([]);
+ expect(root).toMatchRenderedOutput(null);
});
it("does not infinite loop if there's a render phase update in the same render as an error", async () => {
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
index d4c7949580c97..fb3c26183fc17 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
@@ -249,18 +249,7 @@ describe('ReactIncrementalErrorLogging', () => {
,
);
- await waitForAll(
- [
- 'render: 0',
-
- 'render: 1',
-
- // Retry one more time before handling error
- 'render: 1',
-
- 'componentWillUnmount: 0',
- ].filter(Boolean),
- );
+ await waitForAll(['render: 0', 'render: 1', 'componentWillUnmount: 0']);
expect(console.error).toHaveBeenCalledTimes(1);
if (__DEV__) {
diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
index 033156d25f0fb..929ff25b889ba 100644
--- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
@@ -56,11 +56,7 @@ describe 'ReactCoffeeScriptClass', ->
expect(->
ReactDOM.flushSync ->
root.render React.createElement(Foo)
- ).toErrorDev([
- # A failed component renders twice in DEV in concurrent mode
- 'No `render` method found on the Foo instance',
- 'No `render` method found on the Foo instance',
- ])
+ ).toErrorDev(['No `render` method found on the Foo instance'])
window.removeEventListener 'error', errorHandler;
expect(caughtErrors).toEqual([
expect.objectContaining(
diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js
index e7226dc0bee4e..0d74286237dff 100644
--- a/packages/react/src/__tests__/ReactES6Class-test.js
+++ b/packages/react/src/__tests__/ReactES6Class-test.js
@@ -70,9 +70,6 @@ describe('ReactES6Class', () => {
expect(() => {
ReactDOM.flushSync(() => root.render());
}).toErrorDev([
- // A failed component renders twice in DEV in concurrent mode
- 'Warning: No `render` method found on the Foo instance: ' +
- 'you may have forgotten to define `render`.',
'Warning: No `render` method found on the Foo instance: ' +
'you may have forgotten to define `render`.',
]);
diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
index 5a6e81bccf329..21ee0b106dcb1 100644
--- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts
@@ -338,11 +338,8 @@ describe('ReactTypeScriptClass', function() {
expect(() => {
ReactDOM.flushSync(() => root.render(React.createElement(Empty)))
}).toErrorDev([
- // A failed component renders twice in DEV in concurrent mode
'Warning: No `render` method found on the Empty instance: ' +
- 'you may have forgotten to define `render`.',
- 'Warning: No `render` method found on the Empty instance: ' +
- 'you may have forgotten to define `render`.',
+ 'you may have forgotten to define `render`.'
]);
} finally {
window.removeEventListener('error', errorHandler);
@@ -445,8 +442,10 @@ describe('ReactTypeScriptClass', function() {
root.render(React.createElement(Foo, {foo: 'foo'}))
);
}).toErrorDev(
- 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
- 'and will be ignored. Instead, declare it as an instance method.'
+ [
+ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
+ 'and will be ignored. Instead, declare it as an instance method.',
+ ]
);
});
diff --git a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
index e01ef57c10a64..815b9af4153be 100644
--- a/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+++ b/packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
@@ -588,11 +588,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
}
assertLog(
- gate(flags => flags.enableUseSyncExternalStoreShim)
+ gate(flags => flags.enableUseSyncExternalStoreShim) ||
+ gate(flags => flags.enableUnifiedSyncLane)
? ['Error in getSnapshot']
: [
'Error in getSnapshot',
- // In a concurrent root, React renders a second time to attempt to
+ // In a non-blocking update, React renders a second time to attempt to
// recover from the error.
'Error in getSnapshot',
],