@samitouri / QOS-React / commits / d3b26b2953

[compiler] rebase #32285 (#34102)

Redo of #32285 which was created with ghstack and is tedious to rebase with sapling.

Joseph Savona committed Aug 4, 2025 at 12:04 UTC d3b26b2953c7e780abaa49f422c53fd4cda08e47
5 files changed +293
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+5
@@ -106,6 +106,7 @@ import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
106 import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
107 import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
108 import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
109 +import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
110
111 export type CompilerPipelineValue =
112 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -292,6 +293,10 @@ function runWithEnvironment(
293 validateNoSetStateInRender(hir).unwrap();
294 }
295
296 + if (env.config.validateNoDerivedComputationsInEffects) {
297 + validateNoDerivedComputationsInEffects(hir);
298 + }
299 +
300 if (env.config.validateNoSetStateInEffects) {
301 env.logErrors(validateNoSetStateInEffects(hir));
302 }
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+6
@@ -323,6 +323,12 @@ export const EnvironmentConfigSchema = z.object({
323 */
324 validateNoSetStateInEffects: z.boolean().default(false),
325
326 + /**
327 + * Validates that effects are not used to calculate derived data which could instead be computed
328 + * during render.
329 + */
330 + validateNoDerivedComputationsInEffects: z.boolean().default(false),
331 +
332 /**
333 * Validates against creating JSX within a try block and recommends using an error boundary
334 * instead.
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts new
+230
@@ -0,0 +1,230 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {CompilerError, ErrorSeverity, SourceLocation} from '..';
9 +import {
10 + ArrayExpression,
11 + BlockId,
12 + FunctionExpression,
13 + HIRFunction,
14 + IdentifierId,
15 + isSetStateType,
16 + isUseEffectHookType,
17 +} from '../HIR';
18 +import {
19 + eachInstructionValueOperand,
20 + eachTerminalOperand,
21 +} from '../HIR/visitors';
22 +
23 +/**
24 + * Validates that useEffect is not used for derived computations which could/should
25 + * be performed in render.
26 + *
27 + * See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
28 + *
29 + * Example:
30 + *
31 + * ```
32 + * // 🔴 Avoid: redundant state and unnecessary Effect
33 + * const [fullName, setFullName] = useState('');
34 + * useEffect(() => {
35 + * setFullName(firstName + ' ' + lastName);
36 + * }, [firstName, lastName]);
37 + * ```
38 + *
39 + * Instead use:
40 + *
41 + * ```
42 + * // ✅ Good: calculated during rendering
43 + * const fullName = firstName + ' ' + lastName;
44 + * ```
45 + */
46 +export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
47 + const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
48 + const functions: Map<IdentifierId, FunctionExpression> = new Map();
49 + const locals: Map<IdentifierId, IdentifierId> = new Map();
50 +
51 + const errors = new CompilerError();
52 +
53 + for (const block of fn.body.blocks.values()) {
54 + for (const instr of block.instructions) {
55 + const {lvalue, value} = instr;
56 + if (value.kind === 'LoadLocal') {
57 + locals.set(lvalue.identifier.id, value.place.identifier.id);
58 + } else if (value.kind === 'ArrayExpression') {
59 + candidateDependencies.set(lvalue.identifier.id, value);
60 + } else if (value.kind === 'FunctionExpression') {
61 + functions.set(lvalue.identifier.id, value);
62 + } else if (
63 + value.kind === 'CallExpression' ||
64 + value.kind === 'MethodCall'
65 + ) {
66 + const callee =
67 + value.kind === 'CallExpression' ? value.callee : value.property;
68 + if (
69 + isUseEffectHookType(callee.identifier) &&
70 + value.args.length === 2 &&
71 + value.args[0].kind === 'Identifier' &&
72 + value.args[1].kind === 'Identifier'
73 + ) {
74 + const effectFunction = functions.get(value.args[0].identifier.id);
75 + const deps = candidateDependencies.get(value.args[1].identifier.id);
76 + if (
77 + effectFunction != null &&
78 + deps != null &&
79 + deps.elements.length !== 0 &&
80 + deps.elements.every(element => element.kind === 'Identifier')
81 + ) {
82 + const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
83 + CompilerError.invariant(dep.kind === 'Identifier', {
84 + reason: `Dependency is checked as a place above`,
85 + loc: value.loc,
86 + });
87 + return locals.get(dep.identifier.id) ?? dep.identifier.id;
88 + });
89 + validateEffect(
90 + effectFunction.loweredFunc.func,
91 + dependencies,
92 + errors,
93 + );
94 + }
95 + }
96 + }
97 + }
98 + }
99 + if (errors.hasErrors()) {
100 + throw errors;
101 + }
102 +}
103 +
104 +function validateEffect(
105 + effectFunction: HIRFunction,
106 + effectDeps: Array<IdentifierId>,
107 + errors: CompilerError,
108 +): void {
109 + for (const operand of effectFunction.context) {
110 + if (isSetStateType(operand.identifier)) {
111 + continue;
112 + } else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
113 + continue;
114 + } else {
115 + // Captured something other than the effect dep or setState
116 + return;
117 + }
118 + }
119 + for (const dep of effectDeps) {
120 + if (
121 + effectFunction.context.find(operand => operand.identifier.id === dep) ==
122 + null
123 + ) {
124 + // effect dep wasn't actually used in the function
125 + return;
126 + }
127 + }
128 +
129 + const seenBlocks: Set<BlockId> = new Set();
130 + const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
131 + for (const dep of effectDeps) {
132 + values.set(dep, [dep]);
133 + }
134 +
135 + const setStateLocations: Array<SourceLocation> = [];
136 + for (const block of effectFunction.body.blocks.values()) {
137 + for (const pred of block.preds) {
138 + if (!seenBlocks.has(pred)) {
139 + // skip if block has a back edge
140 + return;
141 + }
142 + }
143 + for (const phi of block.phis) {
144 + const aggregateDeps: Set<IdentifierId> = new Set();
145 + for (const operand of phi.operands.values()) {
146 + const deps = values.get(operand.identifier.id);
147 + if (deps != null) {
148 + for (const dep of deps) {
149 + aggregateDeps.add(dep);
150 + }
151 + }
152 + }
153 + if (aggregateDeps.size !== 0) {
154 + values.set(phi.place.identifier.id, Array.from(aggregateDeps));
155 + }
156 + }
157 + for (const instr of block.instructions) {
158 + switch (instr.value.kind) {
159 + case 'Primitive':
160 + case 'JSXText':
161 + case 'LoadGlobal': {
162 + break;
163 + }
164 + case 'LoadLocal': {
165 + const deps = values.get(instr.value.place.identifier.id);
166 + if (deps != null) {
167 + values.set(instr.lvalue.identifier.id, deps);
168 + }
169 + break;
170 + }
171 + case 'ComputedLoad':
172 + case 'PropertyLoad':
173 + case 'BinaryExpression':
174 + case 'TemplateLiteral':
175 + case 'CallExpression':
176 + case 'MethodCall': {
177 + const aggregateDeps: Set<IdentifierId> = new Set();
178 + for (const operand of eachInstructionValueOperand(instr.value)) {
179 + const deps = values.get(operand.identifier.id);
180 + if (deps != null) {
181 + for (const dep of deps) {
182 + aggregateDeps.add(dep);
183 + }
184 + }
185 + }
186 + if (aggregateDeps.size !== 0) {
187 + values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
188 + }
189 +
190 + if (
191 + instr.value.kind === 'CallExpression' &&
192 + isSetStateType(instr.value.callee.identifier) &&
193 + instr.value.args.length === 1 &&
194 + instr.value.args[0].kind === 'Identifier'
195 + ) {
196 + const deps = values.get(instr.value.args[0].identifier.id);
197 + if (deps != null && new Set(deps).size === effectDeps.length) {
198 + setStateLocations.push(instr.value.callee.loc);
199 + } else {
200 + // doesn't depend on any deps
201 + return;
202 + }
203 + }
204 + break;
205 + }
206 + default: {
207 + return;
208 + }
209 + }
210 + }
211 + for (const operand of eachTerminalOperand(block.terminal)) {
212 + if (values.has(operand.identifier.id)) {
213 + //
214 + return;
215 + }
216 + }
217 + seenBlocks.add(block.id);
218 + }
219 +
220 + for (const loc of setStateLocations) {
221 + errors.push({
222 + reason:
223 + 'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
224 + description: null,
225 + severity: ErrorSeverity.InvalidReact,
226 + loc,
227 + suggestions: null,
228 + });
229 + }
230 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md new
+39
@@ -0,0 +1,39 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects
6 +function BadExample() {
7 + const [firstName, setFirstName] = useState('Taylor');
8 + const [lastName, setLastName] = useState('Swift');
9 +
10 + // 🔴 Avoid: redundant state and unnecessary Effect
11 + const [fullName, setFullName] = useState('');
12 + useEffect(() => {
13 + setFullName(capitalize(firstName + ' ' + lastName));
14 + }, [firstName, lastName]);
15 +
16 + return <div>{fullName}</div>;
17 +}
18 +
19 +```
20 +
21 +
22 +## Error
23 +
24 +```
25 +Found 1 error:
26 +
27 +Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
28 +
29 +error.invalid-derived-computation-in-effect.ts:9:4
30 + 7 | const [fullName, setFullName] = useState('');
31 + 8 | useEffect(() => {
32 +> 9 | setFullName(capitalize(firstName + ' ' + lastName));
33 + | ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
34 + 10 | }, [firstName, lastName]);
35 + 11 |
36 + 12 | return <div>{fullName}</div>;
37 +```
38 +
39 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.js new
+13
@@ -0,0 +1,13 @@
1 +// @validateNoDerivedComputationsInEffects
2 +function BadExample() {
3 + const [firstName, setFirstName] = useState('Taylor');
4 + const [lastName, setLastName] = useState('Swift');
5 +
6 + // 🔴 Avoid: redundant state and unnecessary Effect
7 + const [fullName, setFullName] = useState('');
8 + useEffect(() => {
9 + setFullName(capitalize(firstName + ' ' + lastName));
10 + }, [firstName, lastName]);
11 +
12 + return <div>{fullName}</div>;
13 +}