.
+ in header (at **)
+ in div (at **)
+ in Mismatch (at **)",
+ "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.",
+ "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
+ "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
+ ]
+ `);
});
// @gate __DEV__
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 7d98dbd87dc78..fb3341af1ce7b 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -310,13 +310,7 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.log(error.message);
},
});
- await waitForAll([
- 'Suspend',
- 'Component',
- 'Component',
- 'Component',
- 'Component',
- ]);
+ await waitForAll(['Suspend']);
jest.runAllTimers();
// Unchanged
@@ -1391,7 +1385,7 @@ describe('ReactDOMServerPartialHydration', () => {
);
// This will throw it away and rerender.
- await waitForAll(['Child', 'Sibling']);
+ await waitForAll(['Child']);
expect(container.textContent).toBe('Hello');
diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
index 1431205b28f36..4124534f9c38a 100644
--- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
@@ -1000,10 +1000,7 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Render third child, even though an earlier sibling threw.
- 'Normal constructor',
- 'Normal componentWillMount',
- 'Normal render',
+ // Skip the remaining siblings
// Handle the error
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
index fb5c827b3ae27..bcdf2b796134e 100644
--- a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
@@ -1040,10 +1040,7 @@ describe('ReactLegacyErrorBoundaries', () => {
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
- // Render third child, even though an earlier sibling threw.
- 'Normal constructor',
- 'Normal componentWillMount',
- 'Normal render',
+ // Skip the remaining siblings
// Finish mounting with null children
'ErrorBoundary componentDidMount',
// Handle the error
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index 4215cade8f87b..d11a50a856189 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -87,8 +87,6 @@ import {
StaticMask,
MutationMask,
Passive,
- Incomplete,
- ShouldCapture,
ForceClientRender,
} from './ReactFiberFlags';
@@ -739,7 +737,7 @@ function completeDehydratedSuspenseBoundary(
) {
warnIfUnhydratedTailNodes(workInProgress);
resetHydrationState();
- workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
+ workInProgress.flags |= ForceClientRender | DidCapture;
return false;
}
@@ -1146,7 +1144,7 @@ function completeWork(
nextState,
);
if (!fallthroughToNormalSuspensePath) {
- if (workInProgress.flags & ShouldCapture) {
+ if (workInProgress.flags & ForceClientRender) {
// Special case. There were remaining unhydrated nodes. We treat
// this as a mismatch. Revert to client rendering.
return workInProgress;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 53af9b4cae72d..54bc76a3d89ec 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -2528,16 +2528,15 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
- if ((completedWork.flags & Incomplete) !== NoFlags) {
- // This fiber did not complete, because one of its children did not
- // complete. Switch to unwinding the stack instead of completing it.
- //
- // The reason "unwind" and "complete" is interleaved is because when
- // something suspends, we continue rendering the siblings even though
- // they will be replaced by a fallback.
- // TODO: Disable sibling prerendering, then remove this branch.
- unwindUnitOfWork(completedWork);
- return;
+ if (__DEV__) {
+ if ((completedWork.flags & Incomplete) !== NoFlags) {
+ // NOTE: If we re-enable sibling prerendering in some cases, this branch
+ // is where we would switch to the unwinding path.
+ console.error(
+ 'Internal React error: Expected this fiber to be complete, but ' +
+ "it isn't. It should have been unwound. This is a bug in React.",
+ );
+ }
}
// The current, flushed, state of this fiber is the alternate. Ideally
@@ -2640,18 +2639,10 @@ function unwindUnitOfWork(unitOfWork: Fiber): void {
returnFiber.deletions = null;
}
- // If there are siblings, work on them now even though they're going to be
- // replaced by a fallback. We're "prerendering" them. Historically our
- // rationale for this behavior has been to initiate any lazy data requests
- // in the siblings, and also to warm up the CPU cache.
- // TODO: Don't prerender siblings. With `use`, we suspend the work loop
- // until the data has resolved, anyway.
- const siblingFiber = incompleteWork.sibling;
- if (siblingFiber !== null) {
- // This branch will return us to the normal work loop.
- workInProgress = siblingFiber;
- return;
- }
+ // NOTE: If we re-enable sibling prerendering in some cases, this branch
+ // is where we would switch to the normal completion path: check if a
+ // sibling exists, and if so, begin work on it.
+
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null
incompleteWork = returnFiber;
diff --git a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
index d66634830fce7..15f5992109181 100644
--- a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
@@ -111,7 +111,7 @@ describe('ReactBlockingMode', () => {
,
);
- await waitForAll(['A', 'Suspend! [B]', 'C', 'Loading...']);
+ await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
// In Legacy Mode, A and B would mount in a hidden primary tree. In
// Concurrent Mode, nothing in the primary tree should mount. But the
// fallback should mount immediately.
diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js
index 6b1cb7423a4a9..7709652467e86 100644
--- a/packages/react-reconciler/src/__tests__/ReactCache-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js
@@ -1057,7 +1057,7 @@ describe('ReactCache', () => {
await act(() => {
root.render(
);
});
- assertLog(['Cache miss! [A]', 'Cache miss! [B]', 'Loading...']);
+ assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
diff --git a/packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js b/packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
index e14fc83b2a3be..70adde03ddc5c 100644
--- a/packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
@@ -6,6 +6,7 @@ let Suspense;
let getCacheForType;
let startTransition;
let assertLog;
+let waitForPaint;
let caches;
let seededCache;
@@ -23,6 +24,7 @@ describe('ReactConcurrentErrorRecovery', () => {
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
+ waitForPaint = InternalTestUtils.waitForPaint;
getCacheForType = React.unstable_getCacheForType;
@@ -450,20 +452,16 @@ describe('ReactConcurrentErrorRecovery', () => {
await act(() => {
startTransition(() => {
root.render(
-
+ <>
-
- ,
+
+
+
+ >,
);
});
});
- assertLog([
- 'Suspend! [Async]',
- // TODO: Ideally we would skip this second render pass to render the
- // error UI, since it's not going to commit anyway. The same goes for
- // Suspense fallbacks during a refresh transition.
- 'Caught an error: Oops!',
- ]);
+ assertLog(['Suspend! [Async]']);
// The render suspended without committing or surfacing the error.
expect(root).toMatchRenderedOutput(null);
@@ -471,14 +469,16 @@ describe('ReactConcurrentErrorRecovery', () => {
await act(() => {
startTransition(() => {
root.render(
-
-
+ <>
- ,
+
+
+
+ >,
);
});
});
- assertLog(['Suspend! [Async]', 'Caught an error: Oops!']);
+ assertLog(['Suspend! [Async]']);
expect(root).toMatchRenderedOutput(null);
await act(async () => {
@@ -494,7 +494,7 @@ describe('ReactConcurrentErrorRecovery', () => {
'Caught an error: Oops!',
]);
- expect(root).toMatchRenderedOutput('Caught an error: Oops!');
+ expect(root).toMatchRenderedOutput('AsyncCaught an error: Oops!');
},
);
});
diff --git a/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js b/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
index ccde59f41d60f..c565f4a7b6d84 100644
--- a/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactDisableSchedulerTimeoutBasedOnReactExpirationTime-test.internal.js
@@ -72,7 +72,7 @@ describe('ReactSuspenseList', () => {
React.startTransition(() => {
root.render(
);
});
- await waitForAll(['Suspend! [A]', 'Suspend! [B]', 'Loading...']);
+ await waitForAll(['Suspend! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
Scheduler.unstable_advanceTime(2000);
diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
index ae734e19711af..159e17d934623 100644
--- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js
@@ -647,7 +647,7 @@ describe('ReactExpiration', () => {
React.startTransition(() => {
root.render(
);
});
- await waitForAll(['Suspend! [A1]', 'B', 'C', 'Loading...']);
+ await waitForAll(['Suspend! [A1]', 'Loading...']);
// Lots of time elapses before the promise resolves
Scheduler.unstable_advanceTime(10000);
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index 2b274484bc625..2e7bdf0064080 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -99,17 +99,19 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => {
ReactNoop.render(
-
-
+ <>
+
-
-
-
+
+
+
-
- ,
+
+
+
+ >,
);
});
@@ -121,13 +123,6 @@ describe('ReactIncrementalErrorHandling', () => {
'Indirection',
// An error is thrown. React keeps rendering asynchronously.
'throw',
- ]);
-
- // Still rendering async...
- await waitFor(['Indirection']);
-
- await waitFor([
- 'Indirection',
// Call getDerivedStateFromError and re-render the error boundary, this
// time rendering an error message.
@@ -135,14 +130,20 @@ describe('ReactIncrementalErrorHandling', () => {
'ErrorBoundary (catch)',
'ErrorMessage',
]);
-
- // Since the error was thrown during an async render, React won't commit
- // the result yet.
expect(ReactNoop).toMatchRenderedOutput(null);
- // Instead, it will try rendering one more time, synchronously, in case that
- // happens to fix the error.
+ // The work loop unwound to the nearest error boundary. Continue rendering
+ // asynchronously.
+ await waitFor(['Indirection']);
+
+ // Since the error was thrown during an async render, React won't commit the
+ // result yet. After render we render the last child, React will attempt to
+ // render again, synchronously, just in case that happens to fix the error
+ // (i.e. as in the case of a data race). Flush just one more unit of work to
+ // demonstrate that this render is synchronous.
expect(ReactNoop.flushNextYield()).toEqual([
+ 'Indirection',
+
'ErrorBoundary (try)',
'Indirection',
'Indirection',
@@ -151,11 +152,11 @@ describe('ReactIncrementalErrorHandling', () => {
// The error was thrown again. This time, React will actually commit
// the result.
'throw',
- 'Indirection',
- 'Indirection',
'getDerivedStateFromError',
'ErrorBoundary (catch)',
'ErrorMessage',
+ 'Indirection',
+ 'Indirection',
]);
expect(ReactNoop).toMatchRenderedOutput(
@@ -197,17 +198,19 @@ describe('ReactIncrementalErrorHandling', () => {
React.startTransition(() => {
ReactNoop.render(
-
-
+ <>
+
-
-
-
+
+
+
-
- ,
+
+
+
+ >,
);
});
@@ -384,12 +387,11 @@ describe('ReactIncrementalErrorHandling', () => {
// Render the bad component asynchronously
await waitFor(['Parent', 'BadRender']);
- // Finish the rest of the async work
- await waitFor(['Sibling']);
-
- // Old scheduler renders, commits, and throws synchronously
+ // The work loop unwound to the nearest error boundary. React will try
+ // to render one more time, synchronously. Flush just one unit of work to
+ // demonstrate that this render is synchronous.
expect(() => Scheduler.unstable_flushNumberOfYields(1)).toThrow('oops');
- assertLog(['Parent', 'BadRender', 'Sibling', 'commit']);
+ assertLog(['Parent', 'BadRender', 'commit']);
expect(ReactNoop).toMatchRenderedOutput(null);
});
@@ -435,16 +437,12 @@ describe('ReactIncrementalErrorHandling', () => {
// The render expired, but we shouldn't throw out the partial work.
// Finish the current level.
'Oops',
- 'C',
- 'D',
// Since the error occurred during a partially concurrent render, we should
// retry one more time, synchronously.
'A',
'B',
'Oops',
- 'C',
- 'D',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
});
@@ -1571,14 +1569,10 @@ describe('ReactIncrementalErrorHandling', () => {
'ErrorBoundary (try)',
'throw',
// Continue rendering siblings after BadRender throws
- 'BadRenderSibling',
- 'BadRenderSibling',
// React retries one more time
'ErrorBoundary (try)',
'throw',
- 'BadRenderSibling',
- 'BadRenderSibling',
// Errored again on retry. Now handle it.
'componentDidCatch',
diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
index e033f63fa308b..f508ac4abf5b9 100644
--- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -5,6 +5,7 @@ let Scheduler;
let ReactFeatureFlags;
let Suspense;
let lazy;
+let waitFor;
let waitForAll;
let waitForThrow;
let assertLog;
@@ -34,6 +35,7 @@ describe('ReactLazy', () => {
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
+ waitFor = InternalTestUtils.waitFor;
waitForAll = InternalTestUtils.waitForAll;
waitForThrow = InternalTestUtils.waitForThrow;
assertLog = InternalTestUtils.assertLog;
@@ -256,8 +258,14 @@ describe('ReactLazy', () => {
}
}
- const LazyChildA = lazy(() => fakeImport(Child));
- const LazyChildB = lazy(() => fakeImport(Child));
+ const LazyChildA = lazy(() => {
+ Scheduler.log('Suspend! [LazyChildA]');
+ return fakeImport(Child);
+ });
+ const LazyChildB = lazy(() => {
+ Scheduler.log('Suspend! [LazyChildB]');
+ return fakeImport(Child);
+ });
function Parent({swap}) {
return (
@@ -279,11 +287,16 @@ describe('ReactLazy', () => {
unstable_isConcurrent: true,
});
- await waitForAll(['Loading...']);
+ await waitForAll(['Suspend! [LazyChildA]', 'Loading...']);
expect(root).not.toMatchRenderedOutput('AB');
await resolveFakeImport(Child);
+ // B suspends even though it happens to share the same import as A.
+ // TODO: React.lazy should implement the `status` and `value` fields, so
+ // we can unwrap the result synchronously if it already loaded. Like `use`.
+ await waitFor(['A', 'Suspend! [LazyChildB]']);
+
await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
expect(root).toMatchRenderedOutput('AB');
@@ -1389,12 +1402,13 @@ describe('ReactLazy', () => {
unstable_isConcurrent: true,
});
- await waitForAll(['Init A', 'Init B', 'Loading...']);
+ await waitForAll(['Init A', 'Loading...']);
expect(root).not.toMatchRenderedOutput('AB');
await resolveFakeImport(ChildA);
- await resolveFakeImport(ChildB);
+ await waitForAll(['A', 'Init B']);
+ await resolveFakeImport(ChildB);
await waitForAll(['A', 'B', 'Did mount: A', 'Did mount: B']);
expect(root).toMatchRenderedOutput('AB');
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
index d9a8f78d6bd27..df51d2b74d59e 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
@@ -134,8 +134,6 @@ describe('ReactSuspense', () => {
'Bar',
// A suspends
'Suspend! [A]',
- // But we keep rendering the siblings
- 'B',
'Loading...',
]);
expect(root).toMatchRenderedOutput(null);
@@ -272,13 +270,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
- await waitForAll([
- 'Foo',
- 'Suspend! [A]',
- 'Suspend! [B]',
- 'Loading more...',
- 'Loading...',
- ]);
+ await waitForAll(['Foo', 'Suspend! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await resolveText('A');
@@ -316,13 +308,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
- await waitForAll([
- 'Foo',
- 'Suspend! [A]',
- 'Suspend! [B]',
- 'Loading more...',
- 'Loading...',
- ]);
+ await waitForAll(['Foo', 'Suspend! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await resolveText('A');
@@ -937,11 +923,7 @@ describe('ReactSuspense', () => {
unstable_isConcurrent: true,
});
- await waitForAll([
- 'Suspend! [Child 1]',
- 'Suspend! [Child 2]',
- 'Loading...',
- ]);
+ await waitForAll(['Suspend! [Child 1]', 'Loading...']);
await resolveText('Child 1');
await waitForAll(['Child 1', 'Suspend! [Child 2]']);
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js
index 1f612eae67b90..5885ddb0a5969 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js
@@ -126,7 +126,7 @@ describe('ReactSuspense', () => {
ReactNoop.render(element);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
- expect(ops).toEqual([new Set([promise1, promise2])]);
+ expect(ops).toEqual([new Set([promise1])]);
ops = [];
await resolve1();
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
index e8d81970ad5f7..defd59acf6423 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
@@ -266,7 +266,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'App render',
'Text:Inside:Before render',
'Suspend:Async',
- 'ClassText:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
'Text:Fallback create layout',
@@ -641,7 +640,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'App render',
'Text:Inside:Before render',
'Suspend:Async',
- 'Text:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
]);
@@ -796,7 +794,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'App render',
'ClassText:Inside:Before render',
'Suspend:Async',
- 'ClassText:Inside:After render',
'ClassText:Fallback render',
'ClassText:Outside render',
]);
@@ -917,13 +914,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
,
);
- await waitFor([
- 'App render',
- 'Suspend:Async',
- 'Text:Outer render',
- 'Text:Inner render',
- 'Text:Fallback render',
- ]);
+ await waitFor(['App render', 'Suspend:Async', 'Text:Fallback render']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -1047,7 +1038,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await waitFor([
'App render',
'Suspend:Async',
- 'Text:Outer render',
// Text:MemoizedInner is memoized
'Text:Fallback render',
]);
@@ -1186,9 +1176,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
- 'Text:Inner render',
- 'Suspend:InnerAsync_1',
- 'Text:InnerFallback render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
@@ -1208,12 +1195,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('InnerAsync_1');
});
- assertLog([
- 'Text:Outer render',
- 'Suspend:OuterAsync_1',
- 'Text:Inner render',
- 'AsyncText:InnerAsync_1 render',
- ]);
+ assertLog(['Text:Outer render', 'Suspend:OuterAsync_1']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1236,9 +1218,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
- 'Text:Inner render',
- 'Suspend:InnerAsync_2',
- 'Text:InnerFallback render',
'Text:OuterFallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
@@ -1310,8 +1289,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_2',
- 'Text:Inner render',
- 'AsyncText:InnerAsync_2 render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'AsyncText:OuterAsync_1 destroy layout',
@@ -1426,9 +1403,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Suspend:OuterAsync_1',
- 'Text:Inner render',
- 'Suspend:InnerAsync_1',
- 'Text:InnerFallback render',
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
@@ -1922,8 +1896,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
- 'ThrowsInDidMount render',
- 'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInDidMount componentWillUnmount',
@@ -2058,8 +2030,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
- 'ThrowsInWillUnmount render',
- 'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
@@ -2170,8 +2140,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
- 'ThrowsInLayoutEffect render',
- 'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInLayoutEffect useLayoutEffect destroy',
@@ -2307,8 +2275,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
- 'ThrowsInLayoutEffectDestroy render',
- 'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
@@ -2399,8 +2365,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
await waitFor([
'Text:Function render',
'Suspend:Async_1',
- 'Suspend:Async_2',
- 'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
@@ -2435,7 +2399,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Function render',
'AsyncText:Async_1 render',
'Suspend:Async_2',
- 'ClassText:Class render',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2554,7 +2517,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Function render',
'Suspender "A" render',
'Suspend:A',
- 'ClassText:Class render',
'ClassText:Fallback render',
]);
expect(ReactNoop).toMatchRenderedOutput(
@@ -2588,12 +2550,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
await act(async () => {
await resolveText('A');
});
- assertLog([
- 'Text:Function render',
- 'Suspender "B" render',
- 'Suspend:B',
- 'ClassText:Class render',
- ]);
+ assertLog(['Text:Function render', 'Suspender "B" render', 'Suspend:B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -2819,9 +2776,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App render',
'Suspend:Async',
- 'RefCheckerOuter render',
- 'RefCheckerInner:refObject render',
- 'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -2925,11 +2879,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App render',
'Suspend:Async',
- 'RefCheckerOuter render',
- 'ClassComponent:refObject render',
- 'RefCheckerInner:refObject render',
- 'ClassComponent:refCallback render',
- 'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -3029,11 +2978,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App render',
'Suspend:Async',
- 'RefCheckerOuter render',
- 'FunctionComponent render',
- 'RefCheckerInner:refObject render',
- 'FunctionComponent render',
- 'RefCheckerInner:refCallback render',
'Text:Fallback render',
'RefCheckerOuter destroy layout refObject? true refCallback? true',
'RefCheckerInner:refObject destroy layout ref? false',
@@ -3138,7 +3082,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App render',
'Suspend:Async',
- 'RefChecker render',
'Text:Fallback render',
'RefChecker destroy layout ref? true',
'Text:Fallback create layout',
@@ -3252,8 +3195,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ErrorBoundary render: try',
'App render',
'Suspend:Async',
- 'ThrowsInRefCallback render',
- 'Text:Inside render',
'Text:Fallback render',
'Text:Outside render',
'ThrowsInRefCallback refCallback ref? false',
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
index 1f4a9b9b88734..656bd5cacf38b 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
@@ -134,7 +134,7 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render();
- await waitForAll(['A', 'Suspend! [B]', 'C', 'Loading...']);
+ await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
jest.advanceTimersByTime(1000);
@@ -152,7 +152,7 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render();
- await waitForAll(['Suspend! [B2]', 'C', 'Loading...']);
+ await waitForAll(['Suspend! [B2]', 'Loading...']);
// Time out the update
jest.advanceTimersByTime(750);
@@ -196,7 +196,7 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render();
- await waitForAll(['A', 'Suspend! [B]', 'C', 'Loading...']);
+ await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
expect(ReactNoop).not.toMatchRenderedOutput('ABC');
@@ -207,7 +207,7 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render();
- await waitForAll(['A', 'Suspend! [B2]', 'C', 'Loading...']);
+ await waitForAll(['A', 'Suspend! [B2]', 'Loading...']);
// Time out the update
jest.advanceTimersByTime(750);
await waitForAll([]);
@@ -241,7 +241,7 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount
ReactNoop.render();
- await waitForAll(['a', 'Suspend! [b]', 'c', 'Loading...']);
+ await waitForAll(['a', 'Suspend! [b]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(LOADING...);
@@ -252,7 +252,7 @@ describe('ReactSuspensePlaceholder', () => {
// Update
ReactNoop.render();
- await waitForAll(['a', 'Suspend! [b2]', 'c', 'Loading...']);
+ await waitForAll(['a', 'Suspend! [b2]', 'Loading...']);
// Time out the update
jest.advanceTimersByTime(750);
await waitForAll([]);
@@ -344,7 +344,6 @@ describe('ReactSuspensePlaceholder', () => {
'App',
'Suspending',
'Suspend! [Loaded]',
- 'Text',
'Fallback',
]);
// Since this is initial render we immediately commit the fallback. Another test below
@@ -354,8 +353,8 @@ describe('ReactSuspensePlaceholder', () => {
// Initial mount only shows the "Loading..." Fallback.
// The treeBaseDuration then should be 10ms spent rendering Fallback,
- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
- expect(onRender.mock.calls[0][2]).toBe(18);
+ // but the actualDuration should also include the 3ms spent rendering the hidden tree.
+ expect(onRender.mock.calls[0][2]).toBe(13);
expect(onRender.mock.calls[0][3]).toBe(10);
// Resolve the pending promise.
@@ -437,6 +436,30 @@ describe('ReactSuspensePlaceholder', () => {
});
it('properly accounts for base durations when a suspended times out in a concurrent tree', async () => {
+ const Fallback = () => {
+ Scheduler.log('Fallback');
+ Scheduler.unstable_advanceTime(10);
+ return 'Loading...';
+ };
+
+ const Suspending = () => {
+ Scheduler.log('Suspending');
+ Scheduler.unstable_advanceTime(2);
+ return ;
+ };
+
+ App = ({shouldSuspend, text = 'Text', textRenderDuration = 5}) => {
+ Scheduler.log('App');
+ return (
+
+ }>
+ {shouldSuspend && }
+
+
+
+ );
+ };
+
ReactNoop.render(
<>
@@ -463,7 +486,6 @@ describe('ReactSuspensePlaceholder', () => {
'App',
'Suspending',
'Suspend! [Loaded]',
- 'Text',
'Fallback',
]);
expect(ReactNoop).toMatchRenderedOutput('Text');
@@ -475,9 +497,9 @@ describe('ReactSuspensePlaceholder', () => {
// The suspense update should only show the "Loading..." Fallback.
// The actual duration should include 10ms spent rendering Fallback,
- // plus the 8ms render all of the hidden, suspended subtree.
+ // plus the 3ms render all of the partially rendered suspended subtree.
// But the tree base duration should only include 10ms spent rendering Fallback.
- expect(onRender.mock.calls[1][2]).toBe(18);
+ expect(onRender.mock.calls[1][2]).toBe(13);
expect(onRender.mock.calls[1][3]).toBe(10);
// Update again while timed out.
@@ -497,13 +519,12 @@ describe('ReactSuspensePlaceholder', () => {
// consequence of AsyncText relying on the same timer queue as React's
// internal Suspense timer. We should decouple our AsyncText helpers
// from timers.
- Scheduler.unstable_advanceTime(100);
+ Scheduler.unstable_advanceTime(200);
await waitForAll([
'App',
'Suspending',
'Suspend! [Loaded]',
- 'New',
'Fallback',
'Suspend! [Sibling]',
]);
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
index 4d3abff868cb8..a6c139daea7e0 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
@@ -224,11 +224,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Bar',
// A suspends
'Suspend! [A]',
- // But we keep rendering the siblings
- 'B',
+ // We immediately unwind and switch to a fallback without
+ // rendering siblings.
'Loading...',
'C',
- // We leave D incomplete.
+ // Yield before rendering D
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -293,8 +293,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Bar',
// A suspends
'Suspend! [A]',
- // But we keep rendering the siblings
- 'B',
+ // We immediately unwind and switch to a fallback without
+ // rendering siblings.
'Loading...',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -367,7 +367,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// A shell is needed. The update cause it to suspend.
ReactNoop.render(} />);
await waitForAll([]);
- // B suspends. Continue rendering the remaining siblings.
React.startTransition(() => {
ReactNoop.render(
}>
@@ -378,8 +377,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
,
);
});
- // B suspends. Continue rendering the remaining siblings.
- await waitForAll(['A', 'Suspend! [B]', 'C', 'D', 'Loading...']);
+ // B suspends. Render a fallback
+ await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
// Did not commit yet.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -594,7 +593,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render();
});
- await waitForAll(['Suspend! [A]', 'B', 'Loading...']);
+ await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null);
await resolveText('A');
@@ -778,8 +777,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Sync',
// The async content suspends
'Suspend! [Outer content]',
- 'Suspend! [Inner content]',
- 'Loading inner...',
'Loading outer...',
]);
// The outer loading state finishes immediately.
@@ -888,7 +885,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
assertLog([
'Suspend! [Async]',
'Suspend! [Loading (inner)...]',
- 'Sync',
'Loading (outer)...',
]);
// The tree commits synchronously
@@ -1018,7 +1014,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
>,
);
});
- await waitFor(['Suspend! [Async]', 'Sibling']);
+ await waitFor(['Suspend! [Async]']);
await resolveText('Async');
@@ -1059,7 +1055,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
,
);
- await waitForAll(['Suspend! [A]', 'Suspend! [B]', 'Loading...']);
+ await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput();
await resolveText('A');
@@ -1211,7 +1207,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render();
- await waitForAll(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]']);
+ await waitForAll(['Suspend! [A]']);
expect(ReactNoop).toMatchRenderedOutput('Loading...');
await resolveText('A');
@@ -1928,9 +1924,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Foo',
// A suspends
'Suspend! [A]',
- // B suspends
- 'Suspend! [B]',
- 'Loading more...',
'Loading...',
]);
expect(ReactNoop).toMatchRenderedOutput();
@@ -1943,7 +1936,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Retry with the new content.
await waitForAll([
'A',
- // B still suspends
+ // B suspends
'Suspend! [B]',
'Loading more...',
]);
@@ -1989,9 +1982,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Foo',
// A suspends
'Suspend! [A]',
- // B suspends
- 'Suspend! [B]',
- 'Loading more...',
'Loading...',
]);
expect(ReactNoop).toMatchRenderedOutput();
@@ -2001,7 +1991,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Retry with the new content.
await waitForAll([
'A',
- // B still suspends
+ // B suspends
'Suspend! [B]',
'Loading more...',
]);
@@ -2148,7 +2138,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.flushSync(() => showB());
});
- assertLog(['Suspend! [A]', 'Suspend! [B]']);
+ assertLog(['Suspend! [A]']);
});
// TODO: flip to "warns" when this is implemented again.
@@ -2254,7 +2244,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
}
ReactNoop.render();
- await waitForAll(['Foo', 'Suspend! [A]', 'B', 'Initial load...']);
+ await waitForAll(['Foo', 'Suspend! [A]', 'Initial load...']);
expect(ReactNoop).toMatchRenderedOutput();
// Eventually we resolve and show the data.
@@ -3638,7 +3628,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Outer text: B',
'Outer step: 0',
'Suspend! [Inner text: B]',
- 'Inner step: 0',
'Loading...',
]);
// Commit the placeholder
@@ -3667,7 +3656,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Outer text: B',
'Outer step: 1',
'Suspend! [Inner text: B]',
- 'Inner step: 1',
'Loading...',
]);
expect(root).toMatchRenderedOutput(
diff --git a/packages/react-reconciler/src/__tests__/ReactTransition-test.js b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
index 0f63ffe7a1f23..932731095ba39 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransition-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
@@ -860,7 +860,6 @@ describe('ReactTransition', () => {
assertLog([
// Suspend.
'Suspend! [Async]',
- 'Normal pri: 0',
'Loading...',
]);
expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
index ebada20e879ae..b909ceb0e16cf 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
@@ -752,10 +752,6 @@ describe('ReactInteractionTracing', () => {
await waitForAll([
'Suspend [Page Two]',
- 'Suspend [Show Text One]',
- 'Show Text One Loading...',
- 'Suspend [Show Text Two]',
- 'Show Text Two Loading...',
'Loading...',
'onTransitionStart(page transition, 1000)',
'onTransitionProgress(page transition, 1000, 2000, [suspense page])',
@@ -882,10 +878,6 @@ describe('ReactInteractionTracing', () => {
await waitForAll([
'Suspend [Page Two]',
- 'Suspend [Show Text One]',
- 'Show Text One Loading...',
- 'Suspend [Show Text]',
- 'Show Text Loading...',
'Loading...',
'onTransitionStart(navigate, 1000)',
'onTransitionStart(show text one, 1000)',
@@ -1121,8 +1113,6 @@ describe('ReactInteractionTracing', () => {
await waitForAll([
'Suspend [Page Two]',
- 'Suspend [Marker Text]',
- 'Loading...',
'Loading...',
'onTransitionStart(page transition, 1000)',
]);
@@ -1239,10 +1229,6 @@ describe('ReactInteractionTracing', () => {
await waitForAll([
'Suspend [Outer Text]',
- 'Suspend [Inner Text One]',
- 'Inner One...',
- 'Suspend [Inner Text Two]',
- 'Inner Two...',
'Outer...',
'onTransitionStart(page transition, 1000)',
'onMarkerProgress(page transition, outer marker, 1000, 2000, [outer])',
@@ -1782,8 +1768,6 @@ describe('ReactInteractionTracing', () => {
await advanceTimers(1000);
await waitForAll([
'Suspend [Page One]',
- 'Suspend [Child]',
- 'Loading Child...',
'Loading One...',
'Suspend [Page Two]',
'Loading Two...',
diff --git a/packages/react-reconciler/src/__tests__/ReactUse-test.js b/packages/react-reconciler/src/__tests__/ReactUse-test.js
index c013bd50fd384..86bb586ec5829 100644
--- a/packages/react-reconciler/src/__tests__/ReactUse-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactUse-test.js
@@ -352,7 +352,7 @@ describe('ReactUse', () => {
root.render();
});
});
- assertLog(['CD', 'Loading...']);
+ assertLog(['Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
});
@@ -1034,26 +1034,19 @@ describe('ReactUse', () => {
,
);
});
- assertLog([
- 'Async text requested [A]',
- 'Async text requested [B]',
- 'Async text requested [C]',
- '(Loading C...)',
- '(Loading B...)',
- '(Loading A...)',
- ]);
+ assertLog(['Async text requested [A]', '(Loading A...)']);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {
resolveTextRequests('A');
});
- assertLog(['A', '(Loading C...)', '(Loading B...)']);
+ assertLog(['A', 'Async text requested [B]', '(Loading B...)']);
expect(root).toMatchRenderedOutput('A(Loading B...)');
await act(() => {
resolveTextRequests('B');
});
- assertLog(['B', '(Loading C...)']);
+ assertLog(['B', 'Async text requested [C]', '(Loading C...)']);
expect(root).toMatchRenderedOutput('AB(Loading C...)');
await act(() => {
@@ -1087,14 +1080,7 @@ describe('ReactUse', () => {
,
);
});
- assertLog([
- 'Async text requested [A]',
- 'Async text requested [B]',
- 'Async text requested [C]',
- '(Loading C...)',
- '(Loading B...)',
- '(Loading A...)',
- ]);
+ assertLog(['Async text requested [A]', '(Loading A...)']);
expect(root).toMatchRenderedOutput('(Loading A...)');
await act(() => {
@@ -1116,8 +1102,6 @@ describe('ReactUse', () => {
// React does not suspend on the inner requests, because that would
// block A from appearing. Instead it shows a fallback.
'Async text requested [B]',
- 'Async text requested [C]',
- '(Loading C...)',
'(Loading B...)',
]);
expect(root).toMatchRenderedOutput('A(Loading B...)');
diff --git a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js
index 217d88a90298b..38cdae73f0124 100644
--- a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js
+++ b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js
@@ -274,10 +274,7 @@ describe('useEffectEvent', () => {
await waitForThrow(
"A function wrapped in useEffectEvent can't be called during rendering.",
);
-
- // If something throws, we try one more time synchronously in case the error was
- // caused by a data race. See recoverFromConcurrentError
- assertLog(['Count: 0', 'Count: 0']);
+ assertLog([]);
});
// @gate enableUseEffectEventHook