@samitouri / QOS-React / commits / f56dfe950b

Warn on setState() in useInsertionEffect() (#24298)

* Warn on setState() in useInsertionEffect() * Use existing DEV reset mechanism

dan committed Apr 7, 2022 at 20:12 UTC f56dfe950b206651913bf0b836c10592c393357d
5 files changed +157
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+21
@@ -136,6 +136,7 @@ import {
136 restorePendingUpdaters,
137 addTransitionStartCallbackToPendingTransition,
138 addTransitionCompleteCallbackToPendingTransition,
139 + setIsRunningInsertionEffect,
140 } from './ReactFiberWorkLoop.new';
141 import {
142 NoFlags as NoHookEffect,
@@ -536,7 +537,17 @@ function commitHookEffectListUnmount(
537 }
538 }
539
540 + if (__DEV__) {
541 + if ((flags & HookInsertion) !== NoHookEffect) {
542 + setIsRunningInsertionEffect(true);
543 + }
544 + }
545 safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
546 + if (__DEV__) {
547 + if ((flags & HookInsertion) !== NoHookEffect) {
548 + setIsRunningInsertionEffect(false);
549 + }
550 + }
551
552 if (enableSchedulingProfiler) {
553 if ((flags & HookPassive) !== NoHookEffect) {
@@ -570,7 +581,17 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
581
582 // Mount
583 const create = effect.create;
584 + if (__DEV__) {
585 + if ((flags & HookInsertion) !== NoHookEffect) {
586 + setIsRunningInsertionEffect(true);
587 + }
588 + }
589 effect.destroy = create();
590 + if (__DEV__) {
591 + if ((flags & HookInsertion) !== NoHookEffect) {
592 + setIsRunningInsertionEffect(false);
593 + }
594 + }
595
596 if (enableSchedulingProfiler) {
597 if ((flags & HookPassive) !== NoHookEffect) {
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+21
@@ -136,6 +136,7 @@ import {
136 restorePendingUpdaters,
137 addTransitionStartCallbackToPendingTransition,
138 addTransitionCompleteCallbackToPendingTransition,
139 + setIsRunningInsertionEffect,
140 } from './ReactFiberWorkLoop.old';
141 import {
142 NoFlags as NoHookEffect,
@@ -536,7 +537,17 @@ function commitHookEffectListUnmount(
537 }
538 }
539
540 + if (__DEV__) {
541 + if ((flags & HookInsertion) !== NoHookEffect) {
542 + setIsRunningInsertionEffect(true);
543 + }
544 + }
545 safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
546 + if (__DEV__) {
547 + if ((flags & HookInsertion) !== NoHookEffect) {
548 + setIsRunningInsertionEffect(false);
549 + }
550 + }
551
552 if (enableSchedulingProfiler) {
553 if ((flags & HookPassive) !== NoHookEffect) {
@@ -570,7 +581,17 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
581
582 // Mount
583 const create = effect.create;
584 + if (__DEV__) {
585 + if ((flags & HookInsertion) !== NoHookEffect) {
586 + setIsRunningInsertionEffect(true);
587 + }
588 + }
589 effect.destroy = create();
590 + if (__DEV__) {
591 + if ((flags & HookInsertion) !== NoHookEffect) {
592 + setIsRunningInsertionEffect(false);
593 + }
594 + }
595
596 if (enableSchedulingProfiler) {
597 if ((flags & HookPassive) !== NoHookEffect) {
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+17
@@ -409,6 +409,8 @@ let rootWithPassiveNestedUpdates: FiberRoot | null = null;
409 let currentEventTime: number = NoTimestamp;
410 let currentEventTransitionLane: Lanes = NoLanes;
411
412 +let isRunningInsertionEffect = false;
413 +
414 export function getWorkInProgressRoot(): FiberRoot | null {
415 return workInProgressRoot;
416 }
@@ -520,6 +522,12 @@ export function scheduleUpdateOnFiber(
522 ): FiberRoot | null {
523 checkForNestedUpdates();
524
525 + if (__DEV__) {
526 + if (isRunningInsertionEffect) {
527 + console.error('useInsertionEffect must not schedule updates.');
528 + }
529 + }
530 +
531 const root = markUpdateLaneFromFiberToRoot(fiber, lane);
532 if (root === null) {
533 return null;
@@ -2551,6 +2559,9 @@ export function captureCommitPhaseError(
2559 nearestMountedAncestor: Fiber | null,
2560 error: mixed,
2561 ) {
2562 + if (__DEV__) {
2563 + setIsRunningInsertionEffect(false);
2564 + }
2565 if (sourceFiber.tag === HostRoot) {
2566 // Error was thrown at the root. There is no parent, so the root
2567 // itself should capture it.
@@ -3162,3 +3173,9 @@ function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void {
3173 }
3174 }
3175 }
3176 +
3177 +export function setIsRunningInsertionEffect(isRunning: boolean): void {
3178 + if (__DEV__) {
3179 + isRunningInsertionEffect = isRunning;
3180 + }
3181 +}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+17
@@ -409,6 +409,8 @@ let rootWithPassiveNestedUpdates: FiberRoot | null = null;
409 let currentEventTime: number = NoTimestamp;
410 let currentEventTransitionLane: Lanes = NoLanes;
411
412 +let isRunningInsertionEffect = false;
413 +
414 export function getWorkInProgressRoot(): FiberRoot | null {
415 return workInProgressRoot;
416 }
@@ -520,6 +522,12 @@ export function scheduleUpdateOnFiber(
522 ): FiberRoot | null {
523 checkForNestedUpdates();
524
525 + if (__DEV__) {
526 + if (isRunningInsertionEffect) {
527 + console.error('useInsertionEffect must not schedule updates.');
528 + }
529 + }
530 +
531 const root = markUpdateLaneFromFiberToRoot(fiber, lane);
532 if (root === null) {
533 return null;
@@ -2551,6 +2559,9 @@ export function captureCommitPhaseError(
2559 nearestMountedAncestor: Fiber | null,
2560 error: mixed,
2561 ) {
2562 + if (__DEV__) {
2563 + setIsRunningInsertionEffect(false);
2564 + }
2565 if (sourceFiber.tag === HostRoot) {
2566 // Error was thrown at the root. There is no parent, so the root
2567 // itself should capture it.
@@ -3162,3 +3173,9 @@ function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void {
3173 }
3174 }
3175 }
3176 +
3177 +export function setIsRunningInsertionEffect(isRunning: boolean): void {
3178 + if (__DEV__) {
3179 + isRunningInsertionEffect = isRunning;
3180 + }
3181 +}
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+81
@@ -3090,6 +3090,87 @@ describe('ReactHooksWithNoopRenderer', () => {
3090 }),
3091 ).toThrow('is not a function');
3092 });
3093 +
3094 + it('warns when setState is called from insertion effect setup', () => {
3095 + function App(props) {
3096 + const [, setX] = useState(0);
3097 + useInsertionEffect(() => {
3098 + setX(1);
3099 + if (props.throw) {
3100 + throw Error('No');
3101 + }
3102 + }, [props.throw]);
3103 + return null;
3104 + }
3105 +
3106 + const root = ReactNoop.createRoot();
3107 + expect(() =>
3108 + act(() => {
3109 + root.render(<App />);
3110 + }),
3111 + ).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3112 +
3113 + expect(() => {
3114 + act(() => {
3115 + root.render(<App throw={true} />);
3116 + });
3117 + }).toThrow('No');
3118 +
3119 + // Should not warn for regular effects after throw.
3120 + function NotInsertion() {
3121 + const [, setX] = useState(0);
3122 + useEffect(() => {
3123 + setX(1);
3124 + }, []);
3125 + return null;
3126 + }
3127 + act(() => {
3128 + root.render(<NotInsertion />);
3129 + });
3130 + });
3131 +
3132 + it('warns when setState is called from insertion effect cleanup', () => {
3133 + function App(props) {
3134 + const [, setX] = useState(0);
3135 + useInsertionEffect(() => {
3136 + if (props.throw) {
3137 + throw Error('No');
3138 + }
3139 + return () => {
3140 + setX(1);
3141 + };
3142 + }, [props.throw, props.foo]);
3143 + return null;
3144 + }
3145 +
3146 + const root = ReactNoop.createRoot();
3147 + act(() => {
3148 + root.render(<App foo="hello" />);
3149 + });
3150 + expect(() => {
3151 + act(() => {
3152 + root.render(<App foo="goodbye" />);
3153 + });
3154 + }).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);
3155 +
3156 + expect(() => {
3157 + act(() => {
3158 + root.render(<App throw={true} />);
3159 + });
3160 + }).toThrow('No');
3161 +
3162 + // Should not warn for regular effects after throw.
3163 + function NotInsertion() {
3164 + const [, setX] = useState(0);
3165 + useEffect(() => {
3166 + setX(1);
3167 + }, []);
3168 + return null;
3169 + }
3170 + act(() => {
3171 + root.render(<NotInsertion />);
3172 + });
3173 + });
3174 });
3175
3176 describe('useLayoutEffect', () => {