From 5fbe88ab4039742b9ac093691166b05bf14ae402 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 24 Jul 2025 14:43:03 -0700 Subject: [PATCH] [compiler] Validate against setState in all effect types --- .../src/Entrypoint/Pipeline.ts | 6 +++--- .../src/HIR/Environment.ts | 4 ++-- ...siveEffects.ts => ValidateNoSetStateInEffects.ts} | 12 +++++++++--- ...nvalid-setState-in-useEffect-transitive.expect.md | 8 ++++---- .../invalid-setState-in-useEffect-transitive.js | 2 +- .../compiler/invalid-setState-in-useEffect.expect.md | 8 ++++---- .../compiler/invalid-setState-in-useEffect.js | 2 +- ...tState-in-useEffect-listener-transitive.expect.md | 4 ++-- ...alid-setState-in-useEffect-listener-transitive.js | 2 +- .../valid-setState-in-useEffect-listener.expect.md | 4 ++-- .../compiler/valid-setState-in-useEffect-listener.js | 2 +- .../src/__tests__/parseConfigPragma-test.ts | 6 +++--- 12 files changed, 33 insertions(+), 27 deletions(-) rename compiler/packages/babel-plugin-react-compiler/src/Validation/{ValidateNoSetStateInPassiveEffects.ts => ValidateNoSetStateInEffects.ts} (92%) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index c5ca3434b1b5..648ff01ba713 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -94,7 +94,7 @@ import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLoca import {outlineFunctions} from '../Optimization/OutlineFunctions'; import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {lowerContextAccess} from '../Optimization/LowerContextAccess'; -import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; +import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects'; import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement'; import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; import {outlineJSX} from '../Optimization/OutlineJsx'; @@ -292,8 +292,8 @@ function runWithEnvironment( validateNoSetStateInRender(hir).unwrap(); } - if (env.config.validateNoSetStateInPassiveEffects) { - env.logErrors(validateNoSetStateInPassiveEffects(hir)); + if (env.config.validateNoSetStateInEffects) { + env.logErrors(validateNoSetStateInEffects(hir)); } if (env.config.validateNoJSXInTryStatements) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index a552803171ad..5b5b78fc5202 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -318,10 +318,10 @@ export const EnvironmentConfigSchema = z.object({ validateNoSetStateInRender: z.boolean().default(true), /** - * Validates that setState is not called directly within a passive effect (useEffect). + * Validates that setState is not called synchronously within an effect (useEffect and friends). * Scheduling a setState (with an event listener, subscription, etc) is valid. */ - validateNoSetStateInPassiveEffects: z.boolean().default(false), + validateNoSetStateInEffects: z.boolean().default(false), /** * Validates against creating JSX within a try block and recommends using an error boundary diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts similarity index 92% rename from compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts rename to compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index a36c347faa00..e9b0fdb8870e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -11,20 +11,22 @@ import { IdentifierId, isSetStateType, isUseEffectHookType, + isUseInsertionEffectHookType, + isUseLayoutEffectHookType, Place, } from '../HIR'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {Result} from '../Utils/Result'; /** - * Validates against calling setState in the body of a *passive* effect (useEffect), + * Validates against calling setState in the body of an effect (useEffect and friends), * while allowing calling setState in callbacks scheduled by the effect. * * Calling setState during execution of a useEffect triggers a re-render, which is * often bad for performance and frequently has more efficient and straightforward * alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples. */ -export function validateNoSetStateInPassiveEffects( +export function validateNoSetStateInEffects( fn: HIRFunction, ): Result { const setStateFunctions: Map = new Map(); @@ -79,7 +81,11 @@ export function validateNoSetStateInPassiveEffects( instr.value.kind === 'MethodCall' ? instr.value.receiver : instr.value.callee; - if (isUseEffectHookType(callee.identifier)) { + if ( + isUseEffectHookType(callee.identifier) || + isUseLayoutEffectHookType(callee.identifier) || + isUseInsertionEffectHookType(callee.identifier) + ) { const arg = instr.value.args[0]; if (arg !== undefined && arg.kind === 'Identifier') { const setState = setStateFunctions.get(arg.identifier.id); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.expect.md index e116fb4fd988..b09fd352f24f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @loggerTestOnly @validateNoSetStateInPassiveEffects +// @loggerTestOnly @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -24,7 +24,7 @@ function Component() { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { @@ -65,8 +65,8 @@ function _temp(s) { ## Logs ``` -{"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} -{"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} +{"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} +{"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} ``` ### Eval output diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.js index bbf976f0bd4e..50007c0bd13a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect-transitive.js @@ -1,4 +1,4 @@ -// @loggerTestOnly @validateNoSetStateInPassiveEffects +// @loggerTestOnly @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.expect.md index 202071455b74..41390e5ce0a2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @loggerTestOnly @validateNoSetStateInPassiveEffects +// @loggerTestOnly @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -18,7 +18,7 @@ function Component() { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { @@ -45,8 +45,8 @@ function _temp(s) { ## Logs ``` -{"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} -{"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} +{"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} +{"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} ``` ### Eval output diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.js index e526ad82e987..a95d3642cb8d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/invalid-setState-in-useEffect.js @@ -1,4 +1,4 @@ -// @loggerTestOnly @validateNoSetStateInPassiveEffects +// @loggerTestOnly @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md index dd48adcda715..ac55bd046993 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js index 8b1e159071ef..525f3e97d191 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md index 7fdd01fd0a76..a7deed9afb09 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -23,7 +23,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js index ba9720cba9b7..723e4841f6d3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts index 903afe4c20b9..0ee50a0e761f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts @@ -15,11 +15,11 @@ describe('parseConfigPragmaForTests()', () => { // Validate defaults first to make sure that the parser is getting the value from the pragma, // and not just missing it and getting the default value expect(defaultConfig.enableUseTypeAnnotations).toBe(false); - expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false); + expect(defaultConfig.validateNoSetStateInEffects).toBe(false); expect(defaultConfig.validateNoSetStateInRender).toBe(true); const config = parseConfigPragmaForTests( - '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false', + '@enableUseTypeAnnotations @validateNoSetStateInEffects:true @validateNoSetStateInRender:false', {compilationMode: defaultOptions.compilationMode}, ); expect(config).toEqual({ @@ -28,7 +28,7 @@ describe('parseConfigPragmaForTests()', () => { environment: { ...defaultOptions.environment, enableUseTypeAnnotations: true, - validateNoSetStateInPassiveEffects: true, + validateNoSetStateInEffects: true, validateNoSetStateInRender: false, enableResetCacheOnSourceFileChanges: false, },