@samitouri / QOS-React-2 / commits / 09056abde7

[Compiler] Improve error for calculate in render useEffect validation (#34580)

Summary: Change error and update snapshots The error now mentions what values are causing the issue which should provide better context on how to fix the issue --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34580). * __->__ #34580 * #34579 * #34578 * #34577 * #34575 * #34574

Jorge Cabiedes committed Oct 23, 2025 at 11:05 UTC 09056abde76c464f4632f322a0ac30cd3984cee6
35 files changed +1718 -9
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+5
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
103 import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
104 import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
105 import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106 +import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
107 import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
108
109 export type CompilerPipelineValue =
@@ -275,6 +276,10 @@ function runWithEnvironment(
276 validateNoDerivedComputationsInEffects(hir);
277 }
278
279 + if (env.config.validateNoDerivedComputationsInEffects_exp) {
280 + validateNoDerivedComputationsInEffects_exp(hir);
281 + }
282 +
283 if (env.config.validateNoSetStateInEffects) {
284 env.logErrors(validateNoSetStateInEffects(hir, env));
285 }
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+6
@@ -324,6 +324,12 @@ export const EnvironmentConfigSchema = z.object({
324 */
325 validateNoDerivedComputationsInEffects: z.boolean().default(false),
326
327 + /**
328 + * Experimental: Validates that effects are not used to calculate derived data which could instead be computed
329 + * during render. Generates a custom error message for each type of violation.
330 + */
331 + validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
332 +
333 /**
334 * Validates against creating JSX within a try block and recommends using an error boundary
335 * instead.
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts new
+504
@@ -0,0 +1,504 @@
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 {CompilerDiagnostic, CompilerError, Effect} from '..';
9 +import {ErrorCategory} from '../CompilerError';
10 +import {
11 + BlockId,
12 + FunctionExpression,
13 + HIRFunction,
14 + IdentifierId,
15 + isSetStateType,
16 + isUseEffectHookType,
17 + Place,
18 + CallExpression,
19 + Instruction,
20 + isUseStateType,
21 + BasicBlock,
22 + isUseRefType,
23 + GeneratedSource,
24 + SourceLocation,
25 +} from '../HIR';
26 +import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
27 +import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
28 +import {assertExhaustive} from '../Utils/utils';
29 +
30 +type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
31 +
32 +type DerivationMetadata = {
33 + typeOfValue: TypeOfValue;
34 + place: Place;
35 + sourcesIds: Set<IdentifierId>;
36 +};
37 +
38 +type ValidationContext = {
39 + readonly functions: Map<IdentifierId, FunctionExpression>;
40 + readonly errors: CompilerError;
41 + readonly derivationCache: DerivationCache;
42 + readonly effects: Set<HIRFunction>;
43 + readonly setStateCache: Map<string | undefined | null, Array<Place>>;
44 + readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
45 +};
46 +
47 +class DerivationCache {
48 + hasChanges: boolean = false;
49 + cache: Map<IdentifierId, DerivationMetadata> = new Map();
50 +
51 + snapshot(): boolean {
52 + const hasChanges = this.hasChanges;
53 + this.hasChanges = false;
54 + return hasChanges;
55 + }
56 +
57 + addDerivationEntry(
58 + derivedVar: Place,
59 + sourcesIds: Set<IdentifierId>,
60 + typeOfValue: TypeOfValue,
61 + ): void {
62 + let newValue: DerivationMetadata = {
63 + place: derivedVar,
64 + sourcesIds: new Set(),
65 + typeOfValue: typeOfValue ?? 'ignored',
66 + };
67 +
68 + if (sourcesIds !== undefined) {
69 + for (const id of sourcesIds) {
70 + const sourcePlace = this.cache.get(id)?.place;
71 +
72 + if (sourcePlace === undefined) {
73 + continue;
74 + }
75 +
76 + /*
77 + * If the identifier of the source is a promoted identifier, then
78 + * we should set the target as the source.
79 + */
80 + if (
81 + sourcePlace.identifier.name === null ||
82 + sourcePlace.identifier.name?.kind === 'promoted'
83 + ) {
84 + newValue.sourcesIds.add(derivedVar.identifier.id);
85 + } else {
86 + newValue.sourcesIds.add(sourcePlace.identifier.id);
87 + }
88 + }
89 + }
90 +
91 + if (newValue.sourcesIds.size === 0) {
92 + newValue.sourcesIds.add(derivedVar.identifier.id);
93 + }
94 +
95 + const existingValue = this.cache.get(derivedVar.identifier.id);
96 + if (
97 + existingValue === undefined ||
98 + !this.isDerivationEqual(existingValue, newValue)
99 + ) {
100 + this.cache.set(derivedVar.identifier.id, newValue);
101 + this.hasChanges = true;
102 + }
103 + }
104 +
105 + private isDerivationEqual(
106 + a: DerivationMetadata,
107 + b: DerivationMetadata,
108 + ): boolean {
109 + if (a.typeOfValue !== b.typeOfValue) {
110 + return false;
111 + }
112 + if (a.sourcesIds.size !== b.sourcesIds.size) {
113 + return false;
114 + }
115 + for (const id of a.sourcesIds) {
116 + if (!b.sourcesIds.has(id)) {
117 + return false;
118 + }
119 + }
120 + return true;
121 + }
122 +}
123 +
124 +/**
125 + * Validates that useEffect is not used for derived computations which could/should
126 + * be performed in render.
127 + *
128 + * See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
129 + *
130 + * Example:
131 + *
132 + * ```
133 + * // 🔴 Avoid: redundant state and unnecessary Effect
134 + * const [fullName, setFullName] = useState('');
135 + * useEffect(() => {
136 + * setFullName(firstName + ' ' + lastName);
137 + * }, [firstName, lastName]);
138 + * ```
139 + *
140 + * Instead use:
141 + *
142 + * ```
143 + * // ✅ Good: calculated during rendering
144 + * const fullName = firstName + ' ' + lastName;
145 + * ```
146 + */
147 +export function validateNoDerivedComputationsInEffects_exp(
148 + fn: HIRFunction,
149 +): void {
150 + const functions: Map<IdentifierId, FunctionExpression> = new Map();
151 + const derivationCache = new DerivationCache();
152 + const errors = new CompilerError();
153 + const effects: Set<HIRFunction> = new Set();
154 +
155 + const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
156 + const effectSetStateCache: Map<
157 + string | undefined | null,
158 + Array<Place>
159 + > = new Map();
160 +
161 + const context: ValidationContext = {
162 + functions,
163 + errors,
164 + derivationCache,
165 + effects,
166 + setStateCache,
167 + effectSetStateCache,
168 + };
169 +
170 + if (fn.fnType === 'Hook') {
171 + for (const param of fn.params) {
172 + if (param.kind === 'Identifier') {
173 + context.derivationCache.cache.set(param.identifier.id, {
174 + place: param,
175 + sourcesIds: new Set([param.identifier.id]),
176 + typeOfValue: 'fromProps',
177 + });
178 + context.derivationCache.hasChanges = true;
179 + }
180 + }
181 + } else if (fn.fnType === 'Component') {
182 + const props = fn.params[0];
183 + if (props != null && props.kind === 'Identifier') {
184 + context.derivationCache.cache.set(props.identifier.id, {
185 + place: props,
186 + sourcesIds: new Set([props.identifier.id]),
187 + typeOfValue: 'fromProps',
188 + });
189 + context.derivationCache.hasChanges = true;
190 + }
191 + }
192 +
193 + let isFirstPass = true;
194 + do {
195 + for (const block of fn.body.blocks.values()) {
196 + recordPhiDerivations(block, context);
197 + for (const instr of block.instructions) {
198 + recordInstructionDerivations(instr, context, isFirstPass);
199 + }
200 + }
201 +
202 + isFirstPass = false;
203 + } while (context.derivationCache.snapshot());
204 +
205 + for (const effect of effects) {
206 + validateEffect(effect, context);
207 + }
208 +
209 + if (errors.hasAnyErrors()) {
210 + throw errors;
211 + }
212 +}
213 +
214 +function recordPhiDerivations(
215 + block: BasicBlock,
216 + context: ValidationContext,
217 +): void {
218 + for (const phi of block.phis) {
219 + let typeOfValue: TypeOfValue = 'ignored';
220 + let sourcesIds: Set<IdentifierId> = new Set();
221 + for (const operand of phi.operands.values()) {
222 + const operandMetadata = context.derivationCache.cache.get(
223 + operand.identifier.id,
224 + );
225 +
226 + if (operandMetadata === undefined) {
227 + continue;
228 + }
229 +
230 + typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
231 + sourcesIds.add(operand.identifier.id);
232 + }
233 +
234 + if (typeOfValue !== 'ignored') {
235 + context.derivationCache.addDerivationEntry(
236 + phi.place,
237 + sourcesIds,
238 + typeOfValue,
239 + );
240 + }
241 + }
242 +}
243 +
244 +function joinValue(
245 + lvalueType: TypeOfValue,
246 + valueType: TypeOfValue,
247 +): TypeOfValue {
248 + if (lvalueType === 'ignored') return valueType;
249 + if (valueType === 'ignored') return lvalueType;
250 + if (lvalueType === valueType) return lvalueType;
251 + return 'fromPropsAndState';
252 +}
253 +
254 +function recordInstructionDerivations(
255 + instr: Instruction,
256 + context: ValidationContext,
257 + isFirstPass: boolean,
258 +): void {
259 + let typeOfValue: TypeOfValue = 'ignored';
260 + const sources: Set<IdentifierId> = new Set();
261 + const {lvalue, value} = instr;
262 + if (value.kind === 'FunctionExpression') {
263 + context.functions.set(lvalue.identifier.id, value);
264 + for (const [, block] of value.loweredFunc.func.body.blocks) {
265 + for (const instr of block.instructions) {
266 + recordInstructionDerivations(instr, context, isFirstPass);
267 + }
268 + }
269 + } else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
270 + const callee =
271 + value.kind === 'CallExpression' ? value.callee : value.property;
272 + if (
273 + isUseEffectHookType(callee.identifier) &&
274 + value.args.length === 2 &&
275 + value.args[0].kind === 'Identifier' &&
276 + value.args[1].kind === 'Identifier'
277 + ) {
278 + const effectFunction = context.functions.get(value.args[0].identifier.id);
279 + if (effectFunction != null) {
280 + context.effects.add(effectFunction.loweredFunc.func);
281 + }
282 + } else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
283 + const stateValueSource = value.args[0];
284 + if (stateValueSource.kind === 'Identifier') {
285 + sources.add(stateValueSource.identifier.id);
286 + }
287 + typeOfValue = joinValue(typeOfValue, 'fromState');
288 + }
289 + }
290 +
291 + for (const operand of eachInstructionOperand(instr)) {
292 + if (
293 + isSetStateType(operand.identifier) &&
294 + operand.loc !== GeneratedSource &&
295 + isFirstPass
296 + ) {
297 + if (context.setStateCache.has(operand.loc.identifierName)) {
298 + context.setStateCache.get(operand.loc.identifierName)!.push(operand);
299 + } else {
300 + context.setStateCache.set(operand.loc.identifierName, [operand]);
301 + }
302 + }
303 +
304 + const operandMetadata = context.derivationCache.cache.get(
305 + operand.identifier.id,
306 + );
307 +
308 + if (operandMetadata === undefined) {
309 + continue;
310 + }
311 +
312 + typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
313 + for (const id of operandMetadata.sourcesIds) {
314 + sources.add(id);
315 + }
316 + }
317 +
318 + if (typeOfValue === 'ignored') {
319 + return;
320 + }
321 +
322 + for (const lvalue of eachInstructionLValue(instr)) {
323 + context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
324 + }
325 +
326 + for (const operand of eachInstructionOperand(instr)) {
327 + switch (operand.effect) {
328 + case Effect.Capture:
329 + case Effect.Store:
330 + case Effect.ConditionallyMutate:
331 + case Effect.ConditionallyMutateIterator:
332 + case Effect.Mutate: {
333 + if (isMutable(instr, operand)) {
334 + context.derivationCache.addDerivationEntry(
335 + operand,
336 + sources,
337 + typeOfValue,
338 + );
339 + }
340 + break;
341 + }
342 + case Effect.Freeze:
343 + case Effect.Read: {
344 + // no-op
345 + break;
346 + }
347 + case Effect.Unknown: {
348 + CompilerError.invariant(false, {
349 + reason: 'Unexpected unknown effect',
350 + description: null,
351 + details: [
352 + {
353 + kind: 'error',
354 + loc: operand.loc,
355 + message: 'Unexpected unknown effect',
356 + },
357 + ],
358 + });
359 + }
360 + default: {
361 + assertExhaustive(
362 + operand.effect,
363 + `Unexpected effect kind \`${operand.effect}\``,
364 + );
365 + }
366 + }
367 + }
368 +}
369 +
370 +function validateEffect(
371 + effectFunction: HIRFunction,
372 + context: ValidationContext,
373 +): void {
374 + const seenBlocks: Set<BlockId> = new Set();
375 +
376 + const effectDerivedSetStateCalls: Array<{
377 + value: CallExpression;
378 + loc: SourceLocation;
379 + sourceIds: Set<IdentifierId>;
380 + typeOfValue: TypeOfValue;
381 + }> = [];
382 +
383 + const globals: Set<IdentifierId> = new Set();
384 + for (const block of effectFunction.body.blocks.values()) {
385 + for (const pred of block.preds) {
386 + if (!seenBlocks.has(pred)) {
387 + // skip if block has a back edge
388 + return;
389 + }
390 + }
391 +
392 + for (const instr of block.instructions) {
393 + // Early return if any instruction is deriving a value from a ref
394 + if (isUseRefType(instr.lvalue.identifier)) {
395 + return;
396 + }
397 +
398 + for (const operand of eachInstructionOperand(instr)) {
399 + if (
400 + isSetStateType(operand.identifier) &&
401 + operand.loc !== GeneratedSource
402 + ) {
403 + if (context.effectSetStateCache.has(operand.loc.identifierName)) {
404 + context.effectSetStateCache
405 + .get(operand.loc.identifierName)!
406 + .push(operand);
407 + } else {
408 + context.effectSetStateCache.set(operand.loc.identifierName, [
409 + operand,
410 + ]);
411 + }
412 + }
413 + }
414 +
415 + if (
416 + instr.value.kind === 'CallExpression' &&
417 + isSetStateType(instr.value.callee.identifier) &&
418 + instr.value.args.length === 1 &&
419 + instr.value.args[0].kind === 'Identifier'
420 + ) {
421 + const argMetadata = context.derivationCache.cache.get(
422 + instr.value.args[0].identifier.id,
423 + );
424 +
425 + if (argMetadata !== undefined) {
426 + effectDerivedSetStateCalls.push({
427 + value: instr.value,
428 + loc: instr.value.callee.loc,
429 + sourceIds: argMetadata.sourcesIds,
430 + typeOfValue: argMetadata.typeOfValue,
431 + });
432 + }
433 + } else if (instr.value.kind === 'CallExpression') {
434 + const calleeMetadata = context.derivationCache.cache.get(
435 + instr.value.callee.identifier.id,
436 + );
437 +
438 + if (
439 + calleeMetadata !== undefined &&
440 + (calleeMetadata.typeOfValue === 'fromProps' ||
441 + calleeMetadata.typeOfValue === 'fromPropsAndState')
442 + ) {
443 + // If the callee is a prop we can't confidently say that it should be derived in render
444 + return;
445 + }
446 +
447 + if (globals.has(instr.value.callee.identifier.id)) {
448 + // If the callee is a global we can't confidently say that it should be derived in render
449 + return;
450 + }
451 + } else if (instr.value.kind === 'LoadGlobal') {
452 + globals.add(instr.lvalue.identifier.id);
453 + for (const operand of eachInstructionOperand(instr)) {
454 + globals.add(operand.identifier.id);
455 + }
456 + }
457 + }
458 + seenBlocks.add(block.id);
459 + }
460 +
461 + for (const derivedSetStateCall of effectDerivedSetStateCalls) {
462 + if (
463 + derivedSetStateCall.loc !== GeneratedSource &&
464 + context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
465 + context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
466 + context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
467 + .length ===
468 + context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
469 + .length -
470 + 1
471 + ) {
472 + const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
473 + .map(sourceId => {
474 + const sourceMetadata = context.derivationCache.cache.get(sourceId);
475 + return sourceMetadata?.place.identifier.name?.value;
476 + })
477 + .filter(Boolean)
478 + .join(', ');
479 +
480 + let description;
481 +
482 + if (derivedSetStateCall.typeOfValue === 'fromProps') {
483 + description = `From props: [${derivedDepsStr}]`;
484 + } else if (derivedSetStateCall.typeOfValue === 'fromState') {
485 + description = `From local state: [${derivedDepsStr}]`;
486 + } else {
487 + description = `From props and local state: [${derivedDepsStr}]`;
488 + }
489 +
490 + context.errors.pushDiagnostic(
491 + CompilerDiagnostic.create({
492 + description: `Derived values (${description}) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user`,
493 + category: ErrorCategory.EffectDerivationsOfState,
494 + reason:
495 + 'You might not need an effect. Derive values in render, not effects.',
496 + }).withDetails({
497 + kind: 'error',
498 + loc: derivedSetStateCall.value.callee.loc,
499 + message: 'This should be computed during render, not in an effect',
500 + }),
501 + );
502 + }
503 + }
504 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-prop-setter-call-outside-effect-no-error.expect.md new
+84
@@ -0,0 +1,84 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({initialName}) {
9 + const [name, setName] = useState('');
10 +
11 + useEffect(() => {
12 + setName(initialName);
13 + }, [initialName]);
14 +
15 + return (
16 + <div>
17 + <input value={name} onChange={e => setName(e.target.value)} />
18 + </div>
19 + );
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{initialName: 'John'}],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
33 +import { useEffect, useState } from "react";
34 +
35 +function Component(t0) {
36 + const $ = _c(6);
37 + const { initialName } = t0;
38 + const [name, setName] = useState("");
39 + let t1;
40 + let t2;
41 + if ($[0] !== initialName) {
42 + t1 = () => {
43 + setName(initialName);
44 + };
45 + t2 = [initialName];
46 + $[0] = initialName;
47 + $[1] = t1;
48 + $[2] = t2;
49 + } else {
50 + t1 = $[1];
51 + t2 = $[2];
52 + }
53 + useEffect(t1, t2);
54 + let t3;
55 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
56 + t3 = (e) => setName(e.target.value);
57 + $[3] = t3;
58 + } else {
59 + t3 = $[3];
60 + }
61 + let t4;
62 + if ($[4] !== name) {
63 + t4 = (
64 + <div>
65 + <input value={name} onChange={t3} />
66 + </div>
67 + );
68 + $[4] = name;
69 + $[5] = t4;
70 + } else {
71 + t4 = $[5];
72 + }
73 + return t4;
74 +}
75 +
76 +export const FIXTURE_ENTRYPOINT = {
77 + fn: Component,
78 + params: [{ initialName: "John" }],
79 +};
80 +
81 +```
82 +
83 +### Eval output
84 +(kind: ok) <div><input value="John"></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-prop-setter-call-outside-effect-no-error.js new
+21
@@ -0,0 +1,21 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({initialName}) {
5 + const [name, setName] = useState('');
6 +
7 + useEffect(() => {
8 + setName(initialName);
9 + }, [initialName]);
10 +
11 + return (
12 + <div>
13 + <input value={name} onChange={e => setName(e.target.value)} />
14 + </div>
15 + );
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{initialName: 'John'}],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-prop-setter-used-outside-effect-no-error.expect.md new
+85
@@ -0,0 +1,85 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function MockComponent({onSet}) {
9 + return <div onClick={() => onSet('clicked')}>Mock Component</div>;
10 +}
11 +
12 +function Component({propValue}) {
13 + const [value, setValue] = useState(null);
14 + useEffect(() => {
15 + setValue(propValue);
16 + }, [propValue]);
17 +
18 + return <MockComponent onSet={setValue} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{propValue: 'test'}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
32 +import { useEffect, useState } from "react";
33 +
34 +function MockComponent(t0) {
35 + const $ = _c(2);
36 + const { onSet } = t0;
37 + let t1;
38 + if ($[0] !== onSet) {
39 + t1 = <div onClick={() => onSet("clicked")}>Mock Component</div>;
40 + $[0] = onSet;
41 + $[1] = t1;
42 + } else {
43 + t1 = $[1];
44 + }
45 + return t1;
46 +}
47 +
48 +function Component(t0) {
49 + const $ = _c(4);
50 + const { propValue } = t0;
51 + const [, setValue] = useState(null);
52 + let t1;
53 + let t2;
54 + if ($[0] !== propValue) {
55 + t1 = () => {
56 + setValue(propValue);
57 + };
58 + t2 = [propValue];
59 + $[0] = propValue;
60 + $[1] = t1;
61 + $[2] = t2;
62 + } else {
63 + t1 = $[1];
64 + t2 = $[2];
65 + }
66 + useEffect(t1, t2);
67 + let t3;
68 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
69 + t3 = <MockComponent onSet={setValue} />;
70 + $[3] = t3;
71 + } else {
72 + t3 = $[3];
73 + }
74 + return t3;
75 +}
76 +
77 +export const FIXTURE_ENTRYPOINT = {
78 + fn: Component,
79 + params: [{ propValue: "test" }],
80 +};
81 +
82 +```
83 +
84 +### Eval output
85 +(kind: ok) <div>Mock Component</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-prop-setter-used-outside-effect-no-error.js new
+20
@@ -0,0 +1,20 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function MockComponent({onSet}) {
5 + return <div onClick={() => onSet('clicked')}>Mock Component</div>;
6 +}
7 +
8 +function Component({propValue}) {
9 + const [value, setValue] = useState(null);
10 + useEffect(() => {
11 + setValue(propValue);
12 + }, [propValue]);
13 +
14 + return <MockComponent onSet={setValue} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{propValue: 'test'}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-ref-and-state-no-error.expect.md new
+73
@@ -0,0 +1,73 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState, useRef} from 'react';
7 +
8 +export default function Component({test}) {
9 + const [local, setLocal] = useState('');
10 +
11 + const myRef = useRef(null);
12 +
13 + useEffect(() => {
14 + setLocal(myRef.current + test);
15 + }, [test]);
16 +
17 + return <>{local}</>;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{test: 'testString'}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
31 +import { useEffect, useState, useRef } from "react";
32 +
33 +export default function Component(t0) {
34 + const $ = _c(5);
35 + const { test } = t0;
36 + const [local, setLocal] = useState("");
37 +
38 + const myRef = useRef(null);
39 + let t1;
40 + let t2;
41 + if ($[0] !== test) {
42 + t1 = () => {
43 + setLocal(myRef.current + test);
44 + };
45 + t2 = [test];
46 + $[0] = test;
47 + $[1] = t1;
48 + $[2] = t2;
49 + } else {
50 + t1 = $[1];
51 + t2 = $[2];
52 + }
53 + useEffect(t1, t2);
54 + let t3;
55 + if ($[3] !== local) {
56 + t3 = <>{local}</>;
57 + $[3] = local;
58 + $[4] = t3;
59 + } else {
60 + t3 = $[4];
61 + }
62 + return t3;
63 +}
64 +
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: Component,
67 + params: [{ test: "testString" }],
68 +};
69 +
70 +```
71 +
72 +### Eval output
73 +(kind: ok) nulltestString
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/derived-state-from-ref-and-state-no-error.js new
+19
@@ -0,0 +1,19 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState, useRef} from 'react';
3 +
4 +export default function Component({test}) {
5 + const [local, setLocal] = useState('');
6 +
7 + const myRef = useRef(null);
8 +
9 + useEffect(() => {
10 + setLocal(myRef.current + test);
11 + }, [test]);
12 +
13 + return <>{local}</>;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{test: 'testString'}],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-contains-prop-function-call-no-error.expect.md new
+75
@@ -0,0 +1,75 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({propValue, onChange}) {
9 + const [value, setValue] = useState(null);
10 + useEffect(() => {
11 + setValue(propValue);
12 + onChange();
13 + }, [propValue]);
14 +
15 + return <div>{value}</div>;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{propValue: 'test', onChange: () => {}}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
29 +import { useEffect, useState } from "react";
30 +
31 +function Component(t0) {
32 + const $ = _c(7);
33 + const { propValue, onChange } = t0;
34 + const [value, setValue] = useState(null);
35 + let t1;
36 + if ($[0] !== onChange || $[1] !== propValue) {
37 + t1 = () => {
38 + setValue(propValue);
39 + onChange();
40 + };
41 + $[0] = onChange;
42 + $[1] = propValue;
43 + $[2] = t1;
44 + } else {
45 + t1 = $[2];
46 + }
47 + let t2;
48 + if ($[3] !== propValue) {
49 + t2 = [propValue];
50 + $[3] = propValue;
51 + $[4] = t2;
52 + } else {
53 + t2 = $[4];
54 + }
55 + useEffect(t1, t2);
56 + let t3;
57 + if ($[5] !== value) {
58 + t3 = <div>{value}</div>;
59 + $[5] = value;
60 + $[6] = t3;
61 + } else {
62 + t3 = $[6];
63 + }
64 + return t3;
65 +}
66 +
67 +export const FIXTURE_ENTRYPOINT = {
68 + fn: Component,
69 + params: [{ propValue: "test", onChange: () => {} }],
70 +};
71 +
72 +```
73 +
74 +### Eval output
75 +(kind: ok) <div>test</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-contains-prop-function-call-no-error.js new
+17
@@ -0,0 +1,17 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({propValue, onChange}) {
5 + const [value, setValue] = useState(null);
6 + useEffect(() => {
7 + setValue(propValue);
8 + onChange();
9 + }, [propValue]);
10 +
11 + return <div>{value}</div>;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{propValue: 'test', onChange: () => {}}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-with-global-function-call-no-error.expect.md new
+70
@@ -0,0 +1,70 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({propValue}) {
9 + const [value, setValue] = useState(null);
10 + useEffect(() => {
11 + setValue(propValue);
12 + globalCall();
13 + }, [propValue]);
14 +
15 + return <div>{value}</div>;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{propValue: 'test'}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
29 +import { useEffect, useState } from "react";
30 +
31 +function Component(t0) {
32 + const $ = _c(5);
33 + const { propValue } = t0;
34 + const [value, setValue] = useState(null);
35 + let t1;
36 + let t2;
37 + if ($[0] !== propValue) {
38 + t1 = () => {
39 + setValue(propValue);
40 + globalCall();
41 + };
42 + t2 = [propValue];
43 + $[0] = propValue;
44 + $[1] = t1;
45 + $[2] = t2;
46 + } else {
47 + t1 = $[1];
48 + t2 = $[2];
49 + }
50 + useEffect(t1, t2);
51 + let t3;
52 + if ($[3] !== value) {
53 + t3 = <div>{value}</div>;
54 + $[3] = value;
55 + $[4] = t3;
56 + } else {
57 + t3 = $[4];
58 + }
59 + return t3;
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: Component,
64 + params: [{ propValue: "test" }],
65 +};
66 +
67 +```
68 +
69 +### Eval output
70 +(kind: exception) globalCall is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/effect-with-global-function-call-no-error.js new
+17
@@ -0,0 +1,17 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({propValue}) {
5 + const [value, setValue] = useState(null);
6 + useEffect(() => {
7 + setValue(propValue);
8 + globalCall();
9 + }, [propValue]);
10 +
11 + return <div>{value}</div>;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{propValue: 'test'}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-conditionally-in-effect.expect.md new
+49
@@ -0,0 +1,49 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({value, enabled}) {
9 + const [localValue, setLocalValue] = useState('');
10 +
11 + useEffect(() => {
12 + if (enabled) {
13 + setLocalValue(value);
14 + } else {
15 + setLocalValue('disabled');
16 + }
17 + }, [value, enabled]);
18 +
19 + return <div>{localValue}</div>;
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{value: 'test', enabled: true}],
25 +};
26 +
27 +```
28 +
29 +
30 +## Error
31 +
32 +```
33 +Found 1 error:
34 +
35 +Error: You might not need an effect. Derive values in render, not effects.
36 +
37 +Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
38 +
39 +error.derived-state-conditionally-in-effect.ts:9:6
40 + 7 | useEffect(() => {
41 + 8 | if (enabled) {
42 +> 9 | setLocalValue(value);
43 + | ^^^^^^^^^^^^^ This should be computed during render, not in an effect
44 + 10 | } else {
45 + 11 | setLocalValue('disabled');
46 + 12 | }
47 +```
48 +
49 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-conditionally-in-effect.js new
+21
@@ -0,0 +1,21 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({value, enabled}) {
5 + const [localValue, setLocalValue] = useState('');
6 +
7 + useEffect(() => {
8 + if (enabled) {
9 + setLocalValue(value);
10 + } else {
11 + setLocalValue('disabled');
12 + }
13 + }, [value, enabled]);
14 +
15 + return <div>{localValue}</div>;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{value: 'test', enabled: true}],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-default-props.expect.md new
+46
@@ -0,0 +1,46 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +export default function Component({input = 'empty'}) {
9 + const [currInput, setCurrInput] = useState(input);
10 + const localConst = 'local const';
11 +
12 + useEffect(() => {
13 + setCurrInput(input + localConst);
14 + }, [input, localConst]);
15 +
16 + return <div>{currInput}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{input: 'test'}],
22 +};
23 +
24 +```
25 +
26 +
27 +## Error
28 +
29 +```
30 +Found 1 error:
31 +
32 +Error: You might not need an effect. Derive values in render, not effects.
33 +
34 +Derived values (From props: [input]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
35 +
36 +error.derived-state-from-default-props.ts:9:4
37 + 7 |
38 + 8 | useEffect(() => {
39 +> 9 | setCurrInput(input + localConst);
40 + | ^^^^^^^^^^^^ This should be computed during render, not in an effect
41 + 10 | }, [input, localConst]);
42 + 11 |
43 + 12 | return <div>{currInput}</div>;
44 +```
45 +
46 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-default-props.js new
+18
@@ -0,0 +1,18 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +export default function Component({input = 'empty'}) {
5 + const [currInput, setCurrInput] = useState(input);
6 + const localConst = 'local const';
7 +
8 + useEffect(() => {
9 + setCurrInput(input + localConst);
10 + }, [input, localConst]);
11 +
12 + return <div>{currInput}</div>;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{input: 'test'}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-local-state-in-effect.expect.md new
+43
@@ -0,0 +1,43 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +
7 +import {useEffect, useState} from 'react';
8 +
9 +function Component({shouldChange}) {
10 + const [count, setCount] = useState(0);
11 +
12 + useEffect(() => {
13 + if (shouldChange) {
14 + setCount(count + 1);
15 + }
16 + }, [count]);
17 +
18 + return <div>{count}</div>;
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +Found 1 error:
28 +
29 +Error: You might not need an effect. Derive values in render, not effects.
30 +
31 +Derived values (From local state: [count]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
32 +
33 +error.derived-state-from-local-state-in-effect.ts:10:6
34 + 8 | useEffect(() => {
35 + 9 | if (shouldChange) {
36 +> 10 | setCount(count + 1);
37 + | ^^^^^^^^ This should be computed during render, not in an effect
38 + 11 | }
39 + 12 | }, [count]);
40 + 13 |
41 +```
42 +
43 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-local-state-in-effect.js new
+15
@@ -0,0 +1,15 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +
3 +import {useEffect, useState} from 'react';
4 +
5 +function Component({shouldChange}) {
6 + const [count, setCount] = useState(0);
7 +
8 + useEffect(() => {
9 + if (shouldChange) {
10 + setCount(count + 1);
11 + }
12 + }, [count]);
13 +
14 + return <div>{count}</div>;
15 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-local-state-and-component-scope.expect.md new
+53
@@ -0,0 +1,53 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({firstName}) {
9 + const [lastName, setLastName] = useState('Doe');
10 + const [fullName, setFullName] = useState('John');
11 +
12 + const middleName = 'D.';
13 +
14 + useEffect(() => {
15 + setFullName(firstName + ' ' + middleName + ' ' + lastName);
16 + }, [firstName, middleName, lastName]);
17 +
18 + return (
19 + <div>
20 + <input value={lastName} onChange={e => setLastName(e.target.value)} />
21 + <div>{fullName}</div>
22 + </div>
23 + );
24 +}
25 +
26 +export const FIXTURE_ENTRYPOINT = {
27 + fn: Component,
28 + params: [{firstName: 'John'}],
29 +};
30 +
31 +```
32 +
33 +
34 +## Error
35 +
36 +```
37 +Found 1 error:
38 +
39 +Error: You might not need an effect. Derive values in render, not effects.
40 +
41 +Derived values (From props and local state: [firstName, lastName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
42 +
43 +error.derived-state-from-prop-local-state-and-component-scope.ts:11:4
44 + 9 |
45 + 10 | useEffect(() => {
46 +> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
47 + | ^^^^^^^^^^^ This should be computed during render, not in an effect
48 + 12 | }, [firstName, middleName, lastName]);
49 + 13 |
50 + 14 | return (
51 +```
52 +
53 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-local-state-and-component-scope.js new
+25
@@ -0,0 +1,25 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({firstName}) {
5 + const [lastName, setLastName] = useState('Doe');
6 + const [fullName, setFullName] = useState('John');
7 +
8 + const middleName = 'D.';
9 +
10 + useEffect(() => {
11 + setFullName(firstName + ' ' + middleName + ' ' + lastName);
12 + }, [firstName, middleName, lastName]);
13 +
14 + return (
15 + <div>
16 + <input value={lastName} onChange={e => setLastName(e.target.value)} />
17 + <div>{fullName}</div>
18 + </div>
19 + );
20 +}
21 +
22 +export const FIXTURE_ENTRYPOINT = {
23 + fn: Component,
24 + params: [{firstName: 'John'}],
25 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-with-side-effect.expect.md new
+46
@@ -0,0 +1,46 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({value}) {
9 + const [localValue, setLocalValue] = useState('');
10 +
11 + useEffect(() => {
12 + setLocalValue(value);
13 + document.title = `Value: ${value}`;
14 + }, [value]);
15 +
16 + return <div>{localValue}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{value: 'test'}],
22 +};
23 +
24 +```
25 +
26 +
27 +## Error
28 +
29 +```
30 +Found 1 error:
31 +
32 +Error: You might not need an effect. Derive values in render, not effects.
33 +
34 +Derived values (From props: [value]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
35 +
36 +error.derived-state-from-prop-with-side-effect.ts:8:4
37 + 6 |
38 + 7 | useEffect(() => {
39 +> 8 | setLocalValue(value);
40 + | ^^^^^^^^^^^^^ This should be computed during render, not in an effect
41 + 9 | document.title = `Value: ${value}`;
42 + 10 | }, [value]);
43 + 11 |
44 +```
45 +
46 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-with-side-effect.js new
+18
@@ -0,0 +1,18 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({value}) {
5 + const [localValue, setLocalValue] = useState('');
6 +
7 + useEffect(() => {
8 + setLocalValue(value);
9 + document.title = `Value: ${value}`;
10 + }, [value]);
11 +
12 + return <div>{localValue}</div>;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{value: 'test'}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.effect-contains-local-function-call.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component({propValue}) {
9 + const [value, setValue] = useState(null);
10 +
11 + function localFunction() {
12 + console.log('local function');
13 + }
14 +
15 + useEffect(() => {
16 + setValue(propValue);
17 + localFunction();
18 + }, [propValue]);
19 +
20 + return <div>{value}</div>;
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{propValue: 'test'}],
26 +};
27 +
28 +```
29 +
30 +
31 +## Error
32 +
33 +```
34 +Found 1 error:
35 +
36 +Error: You might not need an effect. Derive values in render, not effects.
37 +
38 +Derived values (From props: [propValue]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
39 +
40 +error.effect-contains-local-function-call.ts:12:4
41 + 10 |
42 + 11 | useEffect(() => {
43 +> 12 | setValue(propValue);
44 + | ^^^^^^^^ This should be computed during render, not in an effect
45 + 13 | localFunction();
46 + 14 | }, [propValue]);
47 + 15 |
48 +```
49 +
50 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.effect-contains-local-function-call.js new
+22
@@ -0,0 +1,22 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component({propValue}) {
5 + const [value, setValue] = useState(null);
6 +
7 + function localFunction() {
8 + console.log('local function');
9 + }
10 +
11 + useEffect(() => {
12 + setValue(propValue);
13 + localFunction();
14 + }, [propValue]);
15 +
16 + return <div>{value}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{propValue: 'test'}],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-computation-in-effect.expect.md new
+48
@@ -0,0 +1,48 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +function Component() {
9 + const [firstName, setFirstName] = useState('Taylor');
10 + const lastName = 'Swift';
11 +
12 + // 🔴 Avoid: redundant state and unnecessary Effect
13 + const [fullName, setFullName] = useState('');
14 + useEffect(() => {
15 + setFullName(firstName + ' ' + lastName);
16 + }, [firstName, lastName]);
17 +
18 + return <div>{fullName}</div>;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [],
24 +};
25 +
26 +```
27 +
28 +
29 +## Error
30 +
31 +```
32 +Found 1 error:
33 +
34 +Error: You might not need an effect. Derive values in render, not effects.
35 +
36 +Derived values (From local state: [firstName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
37 +
38 +error.invalid-derived-computation-in-effect.ts:11:4
39 + 9 | const [fullName, setFullName] = useState('');
40 + 10 | useEffect(() => {
41 +> 11 | setFullName(firstName + ' ' + lastName);
42 + | ^^^^^^^^^^^ This should be computed during render, not in an effect
43 + 12 | }, [firstName, lastName]);
44 + 13 |
45 + 14 | return <div>{fullName}</div>;
46 +```
47 +
48 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-computation-in-effect.js new
+20
@@ -0,0 +1,20 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +function Component() {
5 + const [firstName, setFirstName] = useState('Taylor');
6 + const lastName = 'Swift';
7 +
8 + // 🔴 Avoid: redundant state and unnecessary Effect
9 + const [fullName, setFullName] = useState('');
10 + useEffect(() => {
11 + setFullName(firstName + ' ' + lastName);
12 + }, [firstName, lastName]);
13 +
14 + return <div>{fullName}</div>;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-computed-props.expect.md new
+46
@@ -0,0 +1,46 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +export default function Component(props) {
9 + const [displayValue, setDisplayValue] = useState('');
10 +
11 + useEffect(() => {
12 + const computed = props.prefix + props.value + props.suffix;
13 + setDisplayValue(computed);
14 + }, [props.prefix, props.value, props.suffix]);
15 +
16 + return <div>{displayValue}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{prefix: '[', value: 'test', suffix: ']'}],
22 +};
23 +
24 +```
25 +
26 +
27 +## Error
28 +
29 +```
30 +Found 1 error:
31 +
32 +Error: You might not need an effect. Derive values in render, not effects.
33 +
34 +Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
35 +
36 +error.invalid-derived-state-from-computed-props.ts:9:4
37 + 7 | useEffect(() => {
38 + 8 | const computed = props.prefix + props.value + props.suffix;
39 +> 9 | setDisplayValue(computed);
40 + | ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
41 + 10 | }, [props.prefix, props.value, props.suffix]);
42 + 11 |
43 + 12 | return <div>{displayValue}</div>;
44 +```
45 +
46 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-computed-props.js new
+18
@@ -0,0 +1,18 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +export default function Component(props) {
5 + const [displayValue, setDisplayValue] = useState('');
6 +
7 + useEffect(() => {
8 + const computed = props.prefix + props.value + props.suffix;
9 + setDisplayValue(computed);
10 + }, [props.prefix, props.value, props.suffix]);
11 +
12 + return <div>{displayValue}</div>;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{prefix: '[', value: 'test', suffix: ']'}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-destructured-props.expect.md new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState} from 'react';
7 +
8 +export default function Component({props}) {
9 + const [fullName, setFullName] = useState(
10 + props.firstName + ' ' + props.lastName
11 + );
12 +
13 + useEffect(() => {
14 + setFullName(props.firstName + ' ' + props.lastName);
15 + }, [props.firstName, props.lastName]);
16 +
17 + return <div>{fullName}</div>;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{props: {firstName: 'John', lastName: 'Doe'}}],
23 +};
24 +
25 +```
26 +
27 +
28 +## Error
29 +
30 +```
31 +Found 1 error:
32 +
33 +Error: You might not need an effect. Derive values in render, not effects.
34 +
35 +Derived values (From props: [props]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
36 +
37 +error.invalid-derived-state-from-destructured-props.ts:10:4
38 + 8 |
39 + 9 | useEffect(() => {
40 +> 10 | setFullName(props.firstName + ' ' + props.lastName);
41 + | ^^^^^^^^^^^ This should be computed during render, not in an effect
42 + 11 | }, [props.firstName, props.lastName]);
43 + 12 |
44 + 13 | return <div>{fullName}</div>;
45 +```
46 +
47 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.invalid-derived-state-from-destructured-props.js new
+19
@@ -0,0 +1,19 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState} from 'react';
3 +
4 +export default function Component({props}) {
5 + const [fullName, setFullName] = useState(
6 + props.firstName + ' ' + props.lastName
7 + );
8 +
9 + useEffect(() => {
10 + setFullName(props.firstName + ' ' + props.lastName);
11 + }, [props.firstName, props.lastName]);
12 +
13 + return <div>{fullName}</div>;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{props: {firstName: 'John', lastName: 'Doe'}}],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/ref-conditional-in-effect-no-error.expect.md new
+82
@@ -0,0 +1,82 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoDerivedComputationsInEffects_exp
6 +import {useEffect, useState, useRef} from 'react';
7 +
8 +export default function Component({test}) {
9 + const [local, setLocal] = useState(0);
10 +
11 + const myRef = useRef(null);
12 +
13 + useEffect(() => {
14 + if (myRef.current) {
15 + setLocal(test);
16 + } else {
17 + setLocal(test + test);
18 + }
19 + }, [test]);
20 +
21 + return <>{local}</>;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Component,
26 + params: [{test: 4}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
35 +import { useEffect, useState, useRef } from "react";
36 +
37 +export default function Component(t0) {
38 + const $ = _c(5);
39 + const { test } = t0;
40 + const [local, setLocal] = useState(0);
41 +
42 + const myRef = useRef(null);
43 + let t1;
44 + let t2;
45 + if ($[0] !== test) {
46 + t1 = () => {
47 + if (myRef.current) {
48 + setLocal(test);
49 + } else {
50 + setLocal(test + test);
51 + }
52 + };
53 +
54 + t2 = [test];
55 + $[0] = test;
56 + $[1] = t1;
57 + $[2] = t2;
58 + } else {
59 + t1 = $[1];
60 + t2 = $[2];
61 + }
62 + useEffect(t1, t2);
63 + let t3;
64 + if ($[3] !== local) {
65 + t3 = <>{local}</>;
66 + $[3] = local;
67 + $[4] = t3;
68 + } else {
69 + t3 = $[4];
70 + }
71 + return t3;
72 +}
73 +
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: Component,
76 + params: [{ test: 4 }],
77 +};
78 +
79 +```
80 +
81 +### Eval output
82 +(kind: ok) 8
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/ref-conditional-in-effect-no-error.js new
+23
@@ -0,0 +1,23 @@
1 +// @validateNoDerivedComputationsInEffects_exp
2 +import {useEffect, useState, useRef} from 'react';
3 +
4 +export default function Component({test}) {
5 + const [local, setLocal] = useState(0);
6 +
7 + const myRef = useRef(null);
8 +
9 + useEffect(() => {
10 + if (myRef.current) {
11 + setLocal(test);
12 + } else {
13 + setLocal(test + test);
14 + }
15 + }, [test]);
16 +
17 + return <>{local}</>;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{test: 4}],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md
+10 -8
@@ -3,6 +3,8 @@
3
4 ```javascript
5 // @validateNoDerivedComputationsInEffects
6 +import {useEffect, useState} from 'react';
7 +
8 function BadExample() {
9 const [firstName, setFirstName] = useState('Taylor');
10 const [lastName, setLastName] = useState('Swift');
@@ -10,7 +12,7 @@ function BadExample() {
12 // 🔴 Avoid: redundant state and unnecessary Effect
13 const [fullName, setFullName] = useState('');
14 useEffect(() => {
13 - setFullName(capitalize(firstName + ' ' + lastName));
15 + setFullName(firstName + ' ' + lastName);
16 }, [firstName, lastName]);
17
18 return <div>{fullName}</div>;
@@ -26,14 +28,14 @@ Found 1 error:
28
29 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)
30
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));
31 +error.invalid-derived-computation-in-effect.ts:11:4
32 + 9 | const [fullName, setFullName] = useState('');
33 + 10 | useEffect(() => {
34 +> 11 | setFullName(firstName + ' ' + lastName);
35 | ^^^^^^^^^^^ 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>;
36 + 12 | }, [firstName, lastName]);
37 + 13 |
38 + 14 | return <div>{fullName}</div>;
39 ```
40
41
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.js
+3 -1
@@ -1,4 +1,6 @@
1 // @validateNoDerivedComputationsInEffects
2 +import {useEffect, useState} from 'react';
3 +
4 function BadExample() {
5 const [firstName, setFirstName] = useState('Taylor');
6 const [lastName, setLastName] = useState('Swift');
@@ -6,7 +8,7 @@ function BadExample() {
8 // 🔴 Avoid: redundant state and unnecessary Effect
9 const [fullName, setFullName] = useState('');
10 useEffect(() => {
9 - setFullName(capitalize(firstName + ' ' + lastName));
11 + setFullName(firstName + ' ' + lastName);
12 }, [firstName, lastName]);
13
14 return <div>{fullName}</div>;