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', ],