@samitouri / QOS-React-1 / commits / 6f4294af9b

[compiler] Validate against setState in all effect types (#33753)

--- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33753). * #33981 * #33777 * #33767 * #33765 * #33760 * #33759 * #33758 * #33751 * #33752 * __->__ #33753

Joseph Savona committed Jul 24, 2025 at 15:36 UTC 6f4294af9b97c7082289f067315affe2ccf0ed7a
12 files changed +33 -27
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+3 -3
@@ -94,7 +94,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca
94 import {outlineFunctions} from '../Optimization/OutlineFunctions';
95 import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
96 import {lowerContextAccess} from '../Optimization/LowerContextAccess';
97 -import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
97 +import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
98 import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
99 import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
100 import {outlineJSX} from '../Optimization/OutlineJsx';
@@ -292,8 +292,8 @@ function runWithEnvironment(
292 validateNoSetStateInRender(hir).unwrap();
293 }
294
295 - if (env.config.validateNoSetStateInPassiveEffects) {
296 - env.logErrors(validateNoSetStateInPassiveEffects(hir));
295 + if (env.config.validateNoSetStateInEffects) {
296 + env.logErrors(validateNoSetStateInEffects(hir));
297 }
298
299 if (env.config.validateNoJSXInTryStatements) {
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+2 -2
@@ -318,10 +318,10 @@ export const EnvironmentConfigSchema = z.object({
318 validateNoSetStateInRender: z.boolean().default(true),
319
320 /**
321 - * Validates that setState is not called directly within a passive effect (useEffect).
321 + * Validates that setState is not called synchronously within an effect (useEffect and friends).
322 * Scheduling a setState (with an event listener, subscription, etc) is valid.
323 */
324 - validateNoSetStateInPassiveEffects: z.boolean().default(false),
324 + validateNoSetStateInEffects: z.boolean().default(false),
325
326 /**
327 * Validates against creating JSX within a try block and recommends using an error boundary
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts renamed
+9 -3
@@ -11,20 +11,22 @@ import {
11 IdentifierId,
12 isSetStateType,
13 isUseEffectHookType,
14 + isUseInsertionEffectHookType,
15 + isUseLayoutEffectHookType,
16 Place,
17 } from '../HIR';
18 import {eachInstructionValueOperand} from '../HIR/visitors';
19 import {Result} from '../Utils/Result';
20
21 /**
20 - * Validates against calling setState in the body of a *passive* effect (useEffect),
22 + * Validates against calling setState in the body of an effect (useEffect and friends),
23 * while allowing calling setState in callbacks scheduled by the effect.
24 *
25 * Calling setState during execution of a useEffect triggers a re-render, which is
26 * often bad for performance and frequently has more efficient and straightforward
27 * alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
28 */
27 -export function validateNoSetStateInPassiveEffects(
29 +export function validateNoSetStateInEffects(
30 fn: HIRFunction,
31 ): Result<void, CompilerError> {
32 const setStateFunctions: Map<IdentifierId, Place> = new Map();
@@ -79,7 +81,11 @@ export function validateNoSetStateInPassiveEffects(
81 instr.value.kind === 'MethodCall'
82 ? instr.value.receiver
83 : instr.value.callee;
82 - if (isUseEffectHookType(callee.identifier)) {
84 + if (
85 + isUseEffectHookType(callee.identifier) ||
86 + isUseLayoutEffectHookType(callee.identifier) ||
87 + isUseInsertionEffectHookType(callee.identifier)
88 + ) {
89 const arg = instr.value.args[0];
90 if (arg !== undefined && arg.kind === 'Identifier') {
91 const setState = setStateFunctions.get(arg.identifier.id);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.expect.md
+4 -4
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @loggerTestOnly @validateNoSetStateInPassiveEffects
5 +// @loggerTestOnly @validateNoSetStateInEffects
6 import {useEffect, useState} from 'react';
7
8 function Component() {
@@ -24,7 +24,7 @@ function Component() {
24 ## Code
25
26 ```javascript
27 -import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects
27 +import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
28 import { useEffect, useState } from "react";
29
30 function Component() {
@@ -65,8 +65,8 @@ function _temp(s) {
65 ## Logs
66
67 ```
68 -{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":13,"column":4,"index":272},"end":{"line":13,"column":5,"index":273},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"}}},"fnLoc":null}
69 -{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":99},"end":{"line":16,"column":1,"index":300},"filename":"invalid-setState-in-useEffect-transitive.ts"},"fnName":"Component","memoSlots":2,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0}
68 +{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":13,"column":4,"index":265},"end":{"line":13,"column":5,"index":266},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"}}},"fnLoc":null}
69 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":16,"column":1,"index":293},"filename":"invalid-setState-in-useEffect-transitive.ts"},"fnName":"Component","memoSlots":2,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0}
70 ```
71
72 ### Eval output
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @loggerTestOnly @validateNoSetStateInPassiveEffects
1 +// @loggerTestOnly @validateNoSetStateInEffects
2 import {useEffect, useState} from 'react';
3
4 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.expect.md
+4 -4
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @loggerTestOnly @validateNoSetStateInPassiveEffects
5 +// @loggerTestOnly @validateNoSetStateInEffects
6 import {useEffect, useState} from 'react';
7
8 function Component() {
@@ -18,7 +18,7 @@ function Component() {
18 ## Code
19
20 ```javascript
21 -import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects
21 +import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
22 import { useEffect, useState } from "react";
23
24 function Component() {
@@ -45,8 +45,8 @@ function _temp(s) {
45 ## Logs
46
47 ```
48 -{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":7,"column":4,"index":187},"end":{"line":7,"column":12,"index":195},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"}}},"fnLoc":null}
49 -{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":99},"end":{"line":10,"column":1,"index":232},"filename":"invalid-setState-in-useEffect.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
48 +{"kind":"CompileError","detail":{"options":{"reason":"Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":7,"column":4,"index":180},"end":{"line":7,"column":12,"index":188},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"}}},"fnLoc":null}
49 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":10,"column":1,"index":225},"filename":"invalid-setState-in-useEffect.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
50 ```
51
52 ### Eval output
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @loggerTestOnly @validateNoSetStateInPassiveEffects
1 +// @loggerTestOnly @validateNoSetStateInEffects
2 import {useEffect, useState} from 'react';
3
4 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @validateNoSetStateInPassiveEffects
5 +// @validateNoSetStateInEffects
6 import {useEffect, useState} from 'react';
7
8 function Component() {
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
26 ## Code
27
28 ```javascript
29 -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects
29 +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
30 import { useEffect, useState } from "react";
31
32 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @validateNoSetStateInPassiveEffects
1 +// @validateNoSetStateInEffects
2 import {useEffect, useState} from 'react';
3
4 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @validateNoSetStateInPassiveEffects
5 +// @validateNoSetStateInEffects
6 import {useEffect, useState} from 'react';
7
8 function Component() {
@@ -23,7 +23,7 @@ export const FIXTURE_ENTRYPOINT = {
23 ## Code
24
25 ```javascript
26 -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects
26 +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects
27 import { useEffect, useState } from "react";
28
29 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @validateNoSetStateInPassiveEffects
1 +// @validateNoSetStateInEffects
2 import {useEffect, useState} from 'react';
3
4 function Component() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts
+3 -3
@@ -15,11 +15,11 @@ describe('parseConfigPragmaForTests()', () => {
15 // Validate defaults first to make sure that the parser is getting the value from the pragma,
16 // and not just missing it and getting the default value
17 expect(defaultConfig.enableUseTypeAnnotations).toBe(false);
18 - expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false);
18 + expect(defaultConfig.validateNoSetStateInEffects).toBe(false);
19 expect(defaultConfig.validateNoSetStateInRender).toBe(true);
20
21 const config = parseConfigPragmaForTests(
22 - '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false',
22 + '@enableUseTypeAnnotations @validateNoSetStateInEffects:true @validateNoSetStateInRender:false',
23 {compilationMode: defaultOptions.compilationMode},
24 );
25 expect(config).toEqual({
@@ -28,7 +28,7 @@ describe('parseConfigPragmaForTests()', () => {
28 environment: {
29 ...defaultOptions.environment,
30 enableUseTypeAnnotations: true,
31 - validateNoSetStateInPassiveEffects: true,
31 + validateNoSetStateInEffects: true,
32 validateNoSetStateInRender: false,
33 enableResetCacheOnSourceFileChanges: false,
34 },