diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index e2f71c7a4cb1b..31946efd199bd 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -380,7 +380,7 @@ export function scheduleUpdateOnFiber(
expirationTime: ExpirationTime,
) {
checkForNestedUpdates();
- warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
+ warnAboutRenderPhaseUpdatesInDEV(fiber);
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
@@ -2781,30 +2781,44 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
let didWarnAboutUpdateInRender = false;
let didWarnAboutUpdateInGetChildContext = false;
-function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
+function warnAboutRenderPhaseUpdatesInDEV(fiber) {
if (__DEV__) {
- if (fiber.tag === ClassComponent) {
- switch (ReactCurrentDebugFiberPhaseInDEV) {
- case 'getChildContext':
- if (didWarnAboutUpdateInGetChildContext) {
- return;
- }
+ if ((executionContext & RenderContext) !== NoContext) {
+ switch (fiber.tag) {
+ case FunctionComponent:
+ case ForwardRef:
+ case SimpleMemoComponent: {
console.error(
- 'setState(...): Cannot call setState() inside getChildContext()',
+ 'Cannot update a component from inside the function body of a ' +
+ 'different component.',
);
- didWarnAboutUpdateInGetChildContext = true;
break;
- case 'render':
- if (didWarnAboutUpdateInRender) {
- return;
+ }
+ case ClassComponent: {
+ switch (ReactCurrentDebugFiberPhaseInDEV) {
+ case 'getChildContext':
+ if (didWarnAboutUpdateInGetChildContext) {
+ return;
+ }
+ console.error(
+ 'setState(...): Cannot call setState() inside getChildContext()',
+ );
+ didWarnAboutUpdateInGetChildContext = true;
+ break;
+ case 'render':
+ if (didWarnAboutUpdateInRender) {
+ return;
+ }
+ console.error(
+ 'Cannot update during an existing state transition (such as ' +
+ 'within `render`). Render methods should be a pure ' +
+ 'function of props and state.',
+ );
+ didWarnAboutUpdateInRender = true;
+ break;
}
- console.error(
- 'Cannot update during an existing state transition (such as ' +
- 'within `render`). Render methods should be a pure function of ' +
- 'props and state.',
- );
- didWarnAboutUpdateInRender = true;
break;
+ }
}
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
index 2ebf8d16a85e3..f9073f1bf7018 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
@@ -1085,7 +1085,10 @@ describe('ReactHooks', () => {
>,
),
- ).toErrorDev(['Context can only be read while React is rendering']);
+ ).toErrorDev([
+ 'Context can only be read while React is rendering',
+ 'Cannot update a component from inside the function body of a different component.',
+ ]);
});
it('warns when calling hooks inside useReducer', () => {
@@ -1749,8 +1752,9 @@ describe('ReactHooks', () => {
});
// Regression test for #14674
- it('does not swallow original error when updating another component in render phase', () => {
+ it('does not swallow original error when updating another component in render phase', async () => {
let {useState} = React;
+ spyOnDev(console, 'error');
let _setState;
function A() {
@@ -1760,22 +1764,29 @@ describe('ReactHooks', () => {
}
function B() {
- act(() =>
- _setState(() => {
- throw new Error('Hello');
- }),
- );
+ _setState(() => {
+ throw new Error('Hello');
+ });
return null;
}
- expect(() =>
+ await act(async () => {
ReactTestRenderer.create(
<>
>,
- ),
- ).toThrow('Hello');
+ );
+ expect(() => Scheduler.unstable_flushAll()).toThrow('Hello');
+ });
+
+ if (__DEV__) {
+ expect(console.error).toHaveBeenCalledTimes(2);
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'Warning: Cannot update a component from inside the function body ' +
+ 'of a different component.%s',
+ );
+ }
});
// Regression test for https://github.com/facebook/react/issues/15057
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
index 6e50579addd31..325d5ea7e7e00 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
@@ -420,6 +420,50 @@ function loadModules({
]);
});
+ it('warns about render phase update on a different component', async () => {
+ let setStep;
+ function Foo() {
+ const [step, _setStep] = useState(0);
+ setStep = _setStep;
+ return ;
+ }
+
+ function Bar({triggerUpdate}) {
+ if (triggerUpdate) {
+ setStep(1);
+ }
+ return ;
+ }
+
+ const root = ReactNoop.createRoot();
+
+ await ReactNoop.act(async () => {
+ root.render(
+ <>
+
+
+ >,
+ );
+ });
+ expect(Scheduler).toHaveYielded(['Foo [0]', 'Bar']);
+
+ // Bar will update Foo during its render phase. React should warn.
+ await ReactNoop.act(async () => {
+ root.render(
+ <>
+
+
+ >,
+ );
+ expect(() =>
+ expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']),
+ ).toErrorDev([
+ 'Cannot update a component from inside the function body of a ' +
+ 'different component.',
+ ]);
+ });
+ });
+
it('keeps restarting until there are no more new updates', () => {
function Counter({row: newRow}) {
let [count, setCount] = useState(0);