diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 773ab3232a44f..a55b8774c0ac4 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -853,6 +853,75 @@ describe('ReactDOMServerPartialHydration', () => {
expect(span.className).toBe('hi');
});
+ // @gate experimental
+ it('warns but works if setState is called before commit in a dehydrated component', async () => {
+ let suspend = false;
+ let resolve;
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
+
+ let updateText;
+
+ function Child() {
+ const [state, setState] = React.useState('Hello');
+ updateText = setState;
+ Scheduler.unstable_yieldValue('Child');
+ if (suspend) {
+ throw promise;
+ } else {
+ return state;
+ }
+ }
+
+ function Sibling() {
+ Scheduler.unstable_yieldValue('Sibling');
+ return null;
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ suspend = false;
+ const finalHTML = ReactDOMServer.renderToString();
+ expect(Scheduler).toHaveYielded(['Child', 'Sibling']);
+
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ const root = ReactDOM.createRoot(container, {hydrate: true});
+
+ await act(async () => {
+ suspend = true;
+ root.render();
+ expect(Scheduler).toFlushAndYieldThrough(['Child']);
+
+ // While we're part way through the hydration, we update the state.
+ // This will schedule an update on the children of the suspense boundary.
+ expect(() => updateText('Hi')).toErrorDev(
+ "Can't perform a React state update on a component that hasn't mounted yet.",
+ );
+
+ // This will throw it away and rerender.
+ expect(Scheduler).toFlushAndYield(['Child', 'Sibling']);
+
+ expect(container.textContent).toBe('Hello');
+
+ suspend = false;
+ resolve();
+ await promise;
+ });
+ expect(Scheduler).toHaveYielded(['Child', 'Sibling']);
+
+ expect(container.textContent).toBe('Hello');
+ });
+
// @gate experimental
it('blocks the update to hydrate first if context has changed', async () => {
let suspend = false;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 0e5d8c3daf5ff..c7b9e24ceac6d 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -3112,7 +3112,9 @@ function beginWork(
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
- break;
+ // We should never render the children of a dehydrated boundary until we
+ // upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
+ return null;
}
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index 80a8162725556..0a3a4e5a47317 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -1110,6 +1110,8 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
+ // We should never render the children of a dehydrated boundary until we
+ // upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}
@@ -3128,7 +3130,8 @@ function beginWork(
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.effectTag |= DidCapture;
- break;
+
+ return null;
}
}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 3ac10be460a94..e2ff1032b8287 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -76,6 +76,7 @@ import {
} from './ReactTypeOfMode';
import {
HostRoot,
+ IndeterminateComponent,
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
@@ -500,6 +501,14 @@ function markUpdateLaneFromFiberToRoot(
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
+ if (__DEV__) {
+ if (
+ alternate === null &&
+ (fiber.effectTag & (Placement | Hydrating)) !== NoEffect
+ ) {
+ warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
+ }
+ }
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
@@ -508,6 +517,14 @@ function markUpdateLaneFromFiberToRoot(
} else {
while (node !== null) {
alternate = node.alternate;
+ if (__DEV__) {
+ if (
+ alternate === null &&
+ (node.effectTag & (Placement | Hydrating)) !== NoEffect
+ ) {
+ warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
+ }
+ }
node.childLanes = mergeLanes(node.childLanes, lane);
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
@@ -2710,6 +2727,60 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
+let didWarnStateUpdateForNotYetMountedComponent: Set | null = null;
+function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
+ if (__DEV__) {
+ if ((executionContext & RenderContext) !== NoContext) {
+ // We let the other warning about render phase updates deal with this one.
+ return;
+ }
+
+ const tag = fiber.tag;
+ if (
+ tag !== IndeterminateComponent &&
+ tag !== HostRoot &&
+ tag !== ClassComponent &&
+ tag !== FunctionComponent &&
+ tag !== ForwardRef &&
+ tag !== MemoComponent &&
+ tag !== SimpleMemoComponent &&
+ tag !== Block
+ ) {
+ // Only warn for user-defined components, not internal ones like Suspense.
+ return;
+ }
+
+ // We show the whole stack but dedupe on the top component's name because
+ // the problematic code almost always lies inside that component.
+ const componentName = getComponentName(fiber.type) || 'ReactComponent';
+ if (didWarnStateUpdateForNotYetMountedComponent !== null) {
+ if (didWarnStateUpdateForNotYetMountedComponent.has(componentName)) {
+ return;
+ }
+ didWarnStateUpdateForNotYetMountedComponent.add(componentName);
+ } else {
+ didWarnStateUpdateForNotYetMountedComponent = new Set([componentName]);
+ }
+
+ const previousFiber = ReactCurrentFiberCurrent;
+ try {
+ setCurrentDebugFiberInDEV(fiber);
+ console.error(
+ "Can't perform a React state update on a component that hasn't mounted yet. " +
+ 'This indicates that you have a side-effect in your render function that ' +
+ 'asynchronously later calls tries to update the component. Move this work to ' +
+ 'useEffect instead.',
+ );
+ } finally {
+ if (previousFiber) {
+ setCurrentDebugFiberInDEV(fiber);
+ } else {
+ resetCurrentDebugFiberInDEV();
+ }
+ }
+ }
+}
+
let didWarnStateUpdateForUnmountedComponent: Set | null = null;
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
if (__DEV__) {
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 2932e946017ae..e76d0bf38b4f3 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -96,6 +96,7 @@ import {
} from './ReactTypeOfMode';
import {
HostRoot,
+ IndeterminateComponent,
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
@@ -498,6 +499,15 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
if (alternate !== null && alternate.expirationTime < expirationTime) {
alternate.expirationTime = expirationTime;
}
+ if (__DEV__) {
+ if (
+ alternate === null &&
+ (fiber.effectTag & (Placement | Hydrating)) !== NoEffect
+ ) {
+ warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
+ }
+ }
+
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
@@ -506,6 +516,14 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
} else {
while (node !== null) {
alternate = node.alternate;
+ if (__DEV__) {
+ if (
+ alternate === null &&
+ (node.effectTag & (Placement | Hydrating)) !== NoEffect
+ ) {
+ warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
+ }
+ }
if (node.childExpirationTime < expirationTime) {
node.childExpirationTime = expirationTime;
if (
@@ -2901,6 +2919,60 @@ function flushRenderPhaseStrictModeWarningsInDEV() {
}
}
+let didWarnStateUpdateForNotYetMountedComponent: Set | null = null;
+function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
+ if (__DEV__) {
+ if ((executionContext & RenderContext) !== NoContext) {
+ // We let the other warning about render phase updates deal with this one.
+ return;
+ }
+
+ const tag = fiber.tag;
+ if (
+ tag !== IndeterminateComponent &&
+ tag !== HostRoot &&
+ tag !== ClassComponent &&
+ tag !== FunctionComponent &&
+ tag !== ForwardRef &&
+ tag !== MemoComponent &&
+ tag !== SimpleMemoComponent &&
+ tag !== Block
+ ) {
+ // Only warn for user-defined components, not internal ones like Suspense.
+ return;
+ }
+
+ // We show the whole stack but dedupe on the top component's name because
+ // the problematic code almost always lies inside that component.
+ const componentName = getComponentName(fiber.type) || 'ReactComponent';
+ if (didWarnStateUpdateForNotYetMountedComponent !== null) {
+ if (didWarnStateUpdateForNotYetMountedComponent.has(componentName)) {
+ return;
+ }
+ didWarnStateUpdateForNotYetMountedComponent.add(componentName);
+ } else {
+ didWarnStateUpdateForNotYetMountedComponent = new Set([componentName]);
+ }
+
+ const previousFiber = ReactCurrentFiberCurrent;
+ try {
+ setCurrentDebugFiberInDEV(fiber);
+ console.error(
+ "Can't perform a React state update on a component that hasn't mounted yet. " +
+ 'This indicates that you have a side-effect in your render function that ' +
+ 'asynchronously later calls tries to update the component. Move this work to ' +
+ 'useEffect instead.',
+ );
+ } finally {
+ if (previousFiber) {
+ setCurrentDebugFiberInDEV(fiber);
+ } else {
+ resetCurrentDebugFiberInDEV();
+ }
+ }
+ }
+}
+
let didWarnStateUpdateForUnmountedComponent: Set | null = null;
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
if (__DEV__) {