From 78997291302c42b788dafa4c12da246e44be2dda Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:48:27 -0700 Subject: [PATCH] [compiler] Option to treat "set-" prefixed callees as setState functions (#34505) Calling setState functions during render can lead to extraneous renders or even infinite loops. We also have runtime detection for loops, but static detection is obviously even better. This PR adds an option to infer identifers as setState functions if both the following conditions are met: - The identifier is named starting with "set" - The identifier is used as the callee of a call expression By inferring values as SetState type, this allows our existing ValidateNoSetStateInRender rule to flag calls during render, disallowing examples like the following: ```js function Component({setParentState}) { setParentState(...); ^^^^^^^^^^^^^^ Error: Cannot call setState in render } ``` --- .../src/HIR/Environment.ts | 7 +++ .../src/TypeInference/InferTypes.ts | 10 +++- ...-set-state-hook-return-in-render.expect.md | 55 +++++++++++++++++++ ...itional-set-state-hook-return-in-render.js | 14 +++++ ...itional-set-state-prop-in-render.expect.md | 50 +++++++++++++++++ ...-unconditional-set-state-prop-in-render.js | 9 +++ 6 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js 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 0ef41866a51c2..57567f325fd9e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -621,6 +621,13 @@ export const EnvironmentConfigSchema = z.object({ */ enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true), + /** + * Treat identifiers as SetState type if both + * - they are named with a "set-" prefix + * - they are called somewhere + */ + enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false), + /* * If specified a value, the compiler lowers any calls to `useContext` to use * this value as the callee. diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 682195f8df624..1c4443e5a49a0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -31,6 +31,7 @@ import { BuiltInObjectId, BuiltInPropsId, BuiltInRefValueId, + BuiltInSetStateId, BuiltInUseRefId, } from '../HIR/ObjectShape'; import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors'; @@ -276,9 +277,16 @@ function* generateInstructionTypes( * We should change Hook to a subtype of Function or change unifier logic. * (see https://github.com/facebook/react-forget/pull/1427) */ + let shapeId: string | null = null; + if (env.config.enableTreatSetIdentifiersAsStateSetters) { + const name = getName(names, value.callee.identifier.id); + if (name.startsWith('set')) { + shapeId = BuiltInSetStateId; + } + } yield equation(value.callee.identifier.type, { kind: 'Function', - shapeId: null, + shapeId, return: returnType, isConstructor: false, }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md new file mode 100644 index 0000000000000..fcd2f7c4569e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component() { + const [state, setState] = useCustomState(0); + const aliased = setState; + + setState(1); + aliased(2); + + return state; +} + +function useCustomState(init) { + return useState(init); +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-unconditional-set-state-hook-return-in-render.ts:6:2 + 4 | const aliased = setState; + 5 | +> 6 | setState(1); + | ^^^^^^^^ Found setState() in render + 7 | aliased(2); + 8 | + 9 | return state; + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-unconditional-set-state-hook-return-in-render.ts:7:2 + 5 | + 6 | setState(1); +> 7 | aliased(2); + | ^^^^^^^ Found setState() in render + 8 | + 9 | return state; + 10 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js new file mode 100644 index 0000000000000..ed9158ff09681 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-hook-return-in-render.js @@ -0,0 +1,14 @@ +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component() { + const [state, setState] = useCustomState(0); + const aliased = setState; + + setState(1); + aliased(2); + + return state; +} + +function useCustomState(init) { + return useState(init); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md new file mode 100644 index 0000000000000..1a3eb1b7c6a92 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component({setX}) { + const aliased = setX; + + setX(1); + aliased(2); + + return x; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-unconditional-set-state-prop-in-render.ts:5:2 + 3 | const aliased = setX; + 4 | +> 5 | setX(1); + | ^^^^ Found setState() in render + 6 | aliased(2); + 7 | + 8 | return x; + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-unconditional-set-state-prop-in-render.ts:6:2 + 4 | + 5 | setX(1); +> 6 | aliased(2); + | ^^^^^^^ Found setState() in render + 7 | + 8 | return x; + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js new file mode 100644 index 0000000000000..1a14c33fe3807 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unconditional-set-state-prop-in-render.js @@ -0,0 +1,9 @@ +// @validateNoSetStateInRender @enableTreatSetIdentifiersAsStateSetters +function Component({setX}) { + const aliased = setX; + + setX(1); + aliased(2); + + return x; +}