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', () => {