mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Warn if calling setState outside of render but before commit (#18838)
* Don't attempt to render the children of a dehydrated Suspense boundary The DehydratedFragment tag doesn't exist so doing so throws. This can happen if we schedule childExpirationTime on the boundary and bail out. * Warn if scheduling work on a component before it is committed
This commit is contained in:
committed by
GitHub
parent
7b758c183a
commit
2be3433127
@@ -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 (
|
||||
<div>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
<Sibling />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
suspend = false;
|
||||
const finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
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(<App />);
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> | 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<string> | null = null;
|
||||
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -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<string> | 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<string> | null = null;
|
||||
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
|
||||
if (__DEV__) {
|
||||
|
||||
Reference in New Issue
Block a user