Compare commits

...

31 Commits

Author SHA1 Message Date
Jorge Cabiedes
5ccbe0535c Improve error message and detail ordering for shadowing case 2025-08-28 14:58:16 -07:00
Jorge Cabiedes Acosta
007e1b016a Tests for onchange 2025-08-28 10:48:06 -07:00
Jorge Cabiedes Acosta
b930e1bc9e Update detail message 2025-08-28 10:25:42 -07:00
Jorge Cabiedes Acosta
b98bf118e2 Add catching useStates that shadow a reactive value 2025-08-28 10:16:19 -07:00
Jorge Cabiedes Acosta
f026ba5362 Fix tests 2025-08-27 08:45:32 -07:00
Jorge Cabiedes
acf70fef44 Improve code quality and update some tests 2025-08-26 14:21:25 -07:00
Jorge Cabiedes
dd604f7eac Further refine validation error messages and add tests 2025-08-26 10:49:12 -07:00
Jorge Cabiedes Acosta
f3885b6087 Improve error messages and update tests 2025-08-26 09:06:27 -07:00
Jorge Cabiedes
bc96765fd7 First code quality pass 2025-08-25 14:46:29 -07:00
Jorge Cabiedes
2f4d412257 Fix tests 2025-08-25 13:48:57 -07:00
Jorge Cabiedes
169c016d9a Remove values check from old validation logic 2025-08-25 13:41:58 -07:00
Jorge Cabiedes Acosta
203ade6598 Remove console logs 2025-08-25 10:14:08 -07:00
Jorge Cabiedes Acosta
bbb20bbc32 Fix innacurate logic in updateDerivationmetadata and add tests for props with default values 2025-08-25 09:06:27 -07:00
Jorge Cabiedes Acosta
b9ee4a3663 Fix useEffect tests and ensure quality 2025-08-24 08:18:19 -07:00
Jorge Cabiedes Acosta
4f6ebea65b Fix invalid deps message 2025-08-24 07:47:19 -07:00
Jorge Cabiedes Acosta
accdcedf54 Small refactor to deal with nested function expressions 2025-08-22 13:19:18 -07:00
Jorge Cabiedes Acosta
4fdb8cafef Handle a local state variable also being derived from local state 2025-08-22 12:25:17 -07:00
Jorge Cabiedes Acosta
9e503cad4b Fix adding duplicate invalid dependencies 2025-08-21 20:49:42 -07:00
Jorge Cabiedes Acosta
8c88cc84fb Remove single line constraint 2025-08-21 15:47:16 -07:00
Jorge Cabiedes
2006d0af33 fix lints 2025-08-21 13:52:02 -07:00
Jorge Cabiedes
455986b949 First functional disambiguated single line validation of no derived computations in effects 2025-08-21 13:23:41 -07:00
Jorge Cabiedes
bccebc2b72 Added validation for local state and refined error messages 2025-08-21 11:19:53 -07:00
Jorge Cabiedes
621408ba25 Added check for if the same invalid setSate within an effect is used elsewhere 2025-08-21 10:42:30 -07:00
Jorge Cabiedes Acosta
e402d44f0e Iterating over catching state outside/inside effect 2025-08-21 09:23:07 -07:00
Jorge Cabiedes
1d243e3ee7 Valadation for values derived from props in useEffect ready 2025-08-19 14:28:02 -07:00
Jorge Cabiedes Acosta
40bf22bb29 Instruction parsing ready, missing FunctionExpression special case 2025-08-19 10:22:46 -07:00
Jorge Cabiedes Acosta
f1f9498238 Basic solution for instruction based prop derivation validation 2025-08-18 10:11:11 -07:00
Jorge Cabiedes
174b25f536 Rewriting validation 2025-08-14 15:34:38 -07:00
Lauren Tan
20027c02f1 [compiler][wip] Extend ValidateNoDerivedComputationsInEffects for props derived effects
This PR adds infra to disambiguate between two types of derived state in effects:
  1. State derived from props
  2. State derived from other state

TODO:
- [ ] Props tracking through destructuring and property access does not seem to be propagated correctly inside of Functions' instructions (or i might be misunderstanding how we track aliasing effects)
- [ ] compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js should be failing
- [ ] Handle "mixed" case where deps flow from at least one prop AND state. Should probably have a different error reason, to aid with categorization
2025-08-14 10:14:41 -07:00
Lauren Tan
fb46e8f680 [compiler] new tests for props derived
Adds some new test cases for ValidateNoDerivedComputationsInEffects.
2025-08-14 10:14:41 -07:00
Jorge Cabiedes Acosta
190118db35 Forbidden variable names validation 2025-08-08 08:50:03 -07:00
26 changed files with 1264 additions and 84 deletions

View File

@@ -5,20 +5,52 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
} from '..';
import {
ArrayExpression,
BasicBlock,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
Instruction,
Place,
isSetStateType,
isUseEffectHookType,
isUseStateType,
GeneratedSource,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {eachInstructionOperand, eachInstructionLValue} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type SetStateCall = {
loc: SourceLocation;
derivedDep: DerivationMetadata;
setStateId: IdentifierId;
};
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sources: Array<Place>;
};
type ErrorMetadata = {
type: TypeOfValue;
description: string | undefined;
loc: SourceLocation;
setStateName: string | undefined | null;
derivedDepsNames: Array<string>;
};
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -47,12 +79,46 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
const shadowingUseState: Map<string, Array<SourceLocation>> = new Map();
const errors = new CompilerError();
const effectSetStates: Map<
string | undefined | null,
Array<Place>
> = new Map();
const setStateCalls: Map<string | undefined | null, Array<Place>> = new Map();
const errors: Array<ErrorMetadata> = [];
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
derivationCache.set(param.identifier.id, {
place: param,
sources: [param],
typeOfValue: 'fromProps',
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
derivationCache.set(props.identifier.id, {
place: props,
sources: [props],
typeOfValue: 'fromProps',
});
}
}
for (const block of fn.body.blocks.values()) {
parseBlockPhi(block, derivationCache);
for (const instr of block.instructions) {
const {lvalue, value} = instr;
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
@@ -65,6 +131,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
@@ -89,6 +156,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
derivationCache,
effectSetStates,
errors,
);
}
@@ -96,43 +165,325 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
if (errors.hasErrors()) {
throw errors;
const compilerError = generateCompilerError(
setStateCalls,
effectSetStates,
shadowingUseState,
errors,
);
if (compilerError.hasErrors()) {
throw compilerError;
}
}
function generateCompilerError(
setStateCalls: Map<string | undefined | null, Array<Place>>,
effectSetStates: Map<string | undefined | null, Array<Place>>,
shadowingUseState: Map<string, Array<SourceLocation>>,
errors: Array<ErrorMetadata>,
): CompilerError {
const throwableErrors = new CompilerError();
for (const error of errors) {
let compilerDiagnostic: CompilerDiagnostic | undefined = undefined;
/*
* If we use a setState from an invalid useEffect elsewhere then we probably have to
* hoist state up, else we should calculate in render
*/
if (
setStateCalls.get(error.setStateName)?.length !=
effectSetStates.get(error.setStateName)?.length &&
error.type !== 'fromState'
) {
compilerDiagnostic = CompilerDiagnostic.create({
description: `The setState within a useEffect is deriving from ${error.description} Instead of shadowing the prop with local state, hoist the state to the parent component and update it there. If you are purposefully initializing state with a prop, and want to update it when a prop changes, do so conditionally in render`,
category: `You might not need an effect. Local state shadows parent state.`,
severity: ErrorSeverity.InvalidReact,
}).withDetail({
kind: 'error',
loc: error.loc,
message: `this derives values from props ${error.type === 'fromPropsOrState' ? 'and local state ' : ''}to synchronize state`,
});
for (const derivedDep of error.derivedDepsNames) {
if (shadowingUseState.has(derivedDep)) {
for (const loc of shadowingUseState.get(derivedDep)!) {
compilerDiagnostic.withDetail({
kind: 'error',
loc: loc,
message: `this useState shadows ${derivedDep}`,
});
}
}
}
for (const [key, setStateCallArray] of effectSetStates) {
if (setStateCallArray.length === 0) {
continue;
}
const nonUseEffectSetStateCalls = setStateCalls.get(key);
if (nonUseEffectSetStateCalls) {
for (const place of nonUseEffectSetStateCalls) {
if (!setStateCallArray.includes(place)) {
compilerDiagnostic.withDetail({
kind: 'error',
loc: place.loc,
message:
'this setState updates the shadowed state, but should call an onChange event from the parent',
});
}
}
}
}
} else {
compilerDiagnostic = CompilerDiagnostic.create({
description: `${error.description} Derived values 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.`,
category: `You might not need an effect. Derive values in render, not effects.`,
severity: ErrorSeverity.InvalidReact,
}).withDetail({
kind: 'error',
loc: error.loc,
message: 'This should be computed during render, not in an effect',
});
}
if (compilerDiagnostic) {
throwableErrors.pushDiagnostic(compilerDiagnostic);
}
}
return throwableErrors;
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsOrState';
}
function updateDerivationMetadata(
target: Place,
sources: Array<DerivationMetadata> | undefined,
typeOfValue: TypeOfValue | undefined,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
let newValue: DerivationMetadata = {
place: target,
sources: [],
typeOfValue: typeOfValue ?? 'ignored',
};
if (sources !== undefined) {
for (const source of sources) {
/*
* If the identifier of the source is a promoted identifier, then
* we should set the target as the source.
*/
for (const place of source.sources) {
if (
place.identifier.name === null ||
place.identifier.name?.kind === 'promoted'
) {
newValue.sources.push(target);
} else {
newValue.sources.push(place);
}
}
}
}
derivationCache.set(target.identifier.id, newValue);
}
function parseInstr(
instr: Instruction,
derivationCache: Map<IdentifierId, DerivationMetadata>,
setStateCalls: Map<string | undefined | null, Array<Place>>,
shadowingUseState: Map<string, Array<SourceLocation>>,
): void {
// Recursively parse function expressions
if (instr.value.kind === 'FunctionExpression') {
for (const [, block] of instr.value.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
parseInstr(instr, derivationCache, setStateCalls, shadowingUseState);
}
}
}
let typeOfValue: TypeOfValue = 'ignored';
let sources: Array<DerivationMetadata> = [];
// Catch setState calls
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource
) {
if (setStateCalls.has(instr.value.callee.loc.identifierName)) {
setStateCalls
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
setStateCalls.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
}
}
for (const operand of eachInstructionOperand(instr)) {
const opSource = derivationCache.get(operand.identifier.id);
if (opSource === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
sources.push(opSource);
if (
instr.value.kind === 'Destructure' &&
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
isUseStateType(instr.value.value.identifier) &&
opSource.typeOfValue === 'fromProps'
) {
opSource.sources.forEach(source => {
if (instr.value.kind !== 'Destructure') {
return;
}
if (source.identifier.name !== null) {
if (shadowingUseState.has(source.identifier.name.value)) {
shadowingUseState
.get(source.identifier.name.value)
?.push(instr.value.value.loc);
} else {
shadowingUseState.set(source.identifier.name.value, [
instr.value.value.loc,
]);
}
}
});
}
}
// Catch useState hook calls
if (
instr.value.kind === 'Destructure' &&
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
isUseStateType(instr.value.value.identifier)
) {
const stateValueSource = instr.value.lvalue.pattern.items[0];
if (stateValueSource.kind === 'Identifier') {
sources.push({
place: stateValueSource,
typeOfValue: typeOfValue,
sources: [stateValueSource],
});
}
typeOfValue = joinValue(typeOfValue, 'fromState');
}
if (typeOfValue !== 'ignored') {
for (const lvalue of eachInstructionLValue(instr)) {
updateDerivationMetadata(lvalue, sources, typeOfValue, derivationCache);
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
updateDerivationMetadata(
operand,
sources,
typeOfValue,
derivationCache,
);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
loc: operand.loc,
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
}
function parseBlockPhi(
block: BasicBlock,
derivationCache: Map<IdentifierId, DerivationMetadata>,
): void {
for (const phi of block.phis) {
for (const operand of phi.operands.values()) {
const phiSource = derivationCache.get(operand.identifier.id);
if (phiSource !== undefined) {
updateDerivationMetadata(
phi.place,
[phiSource],
phiSource?.typeOfValue,
derivationCache,
);
}
}
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
derivationCache: Map<IdentifierId, DerivationMetadata>,
effectSetStates: Map<string | undefined | null, Array<Place>>,
errors: Array<ErrorMetadata>,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
let isUsingDerivedDeps = false;
for (const dep of effectDeps) {
const depMetadata = derivationCache.get(dep);
if (
effectFunction.context.find(operand => operand.identifier.id === dep) !=
null ||
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
) {
isUsingDerivedDeps = true;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
if (!isUsingDerivedDeps) {
// no prop/state derived deps were used in the body of the effect
return;
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
const derivedSetStateCall: Array<SetStateCall> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
@@ -140,21 +491,29 @@ function validateEffect(
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
parseBlockPhi(block, derivationCache);
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier' &&
instr.value.callee.loc !== GeneratedSource &&
instr.value.callee.loc.identifierName !== undefined &&
instr.value.callee.loc.identifierName !== null
) {
if (effectSetStates.has(instr.value.callee.loc.identifierName)) {
effectSetStates
.get(instr.value.callee.loc.identifierName)!
.push(instr.value.callee);
} else {
effectSetStates.set(instr.value.callee.loc.identifierName, [
instr.value.callee,
]);
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
@@ -162,10 +521,6 @@ function validateEffect(
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
@@ -174,57 +529,61 @@ function validateEffect(
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
const derivedDep = derivationCache.get(
instr.value.args[0].identifier.id,
);
if (derivedDep !== undefined) {
derivedSetStateCall.push({
loc: instr.value.callee.loc,
setStateId: instr.value.callee.identifier.id,
derivedDep: derivedDep,
});
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
for (const call of derivedSetStateCall) {
const derivedDepsStr = Array.from(call.derivedDep.sources)
.map(place => {
return place.identifier.name?.value;
})
.filter(Boolean)
.join(', ');
let errorDescription = '';
if (call.derivedDep.typeOfValue === 'fromProps') {
errorDescription = `props [${derivedDepsStr}].`;
} else if (call.derivedDep.typeOfValue === 'fromState') {
errorDescription = `local state [${derivedDepsStr}].`;
} else {
errorDescription = `both props and local state [${derivedDepsStr}].`;
}
errors.push({
reason:
'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)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
type: call.derivedDep.typeOfValue,
description: `${errorDescription}`,
loc: call.loc,
setStateName:
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
derivedDepsNames: Array.from(call.derivedDep.sources)
.map(place => {
return place.identifier.name?.value ?? '';
})
.filter(Boolean),
});
}
}

View File

@@ -0,0 +1,87 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
import { useEffect, useState } from "react";
function Component(t0) {
const $ = _c(6);
const { initialName } = t0;
const [name, setName] = useState("");
let t1;
if ($[0] !== initialName) {
t1 = () => {
setName(initialName);
};
$[0] = initialName;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[2] = t2;
} else {
t2 = $[2];
}
useEffect(t1, t2);
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = (e) => setName(e.target.value);
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== name) {
t4 = (
<div>
<input value={name} onChange={t3} />
</div>
);
$[4] = name;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ initialName: "John" }],
};
```
### Eval output
(kind: ok) <div><input value="John"></div>

View File

@@ -0,0 +1,21 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({initialName}) {
const [name, setName] = useState('');
useEffect(() => {
setName(initialName);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{initialName: 'John'}],
};

View File

@@ -0,0 +1,51 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from both props and local state [prefix, name]. Derived values 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.
error.bug-derived-state-from-mixed-deps.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setDisplayName(prefix + name);
| ^^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [prefix, name]);
11 |
12 | return (
```

View File

@@ -0,0 +1,23 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({prefix}) {
const [name, setName] = useState('');
const [displayName, setDisplayName] = useState('');
useEffect(() => {
setDisplayName(prefix + name);
}, [prefix, name]);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<div>{displayName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: 'Hello, '}],
};

View File

@@ -0,0 +1,76 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useState, useEffect} from 'react';
function Component({props, number}) {
const nothing = 0;
const missDirection = number;
const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
useEffect(() => {
setDisplayValue(props.prefix + missDirection + nothing);
}, [props.prefix, missDirection, nothing]);
return (
<div
onClick={() => {
setDisplayValue('clicked');
}}>
{displayValue}
</div>
);
}
```
## Error
```
Found 1 error:
Error: Local state shadows parent state.
This setState() appears to derive a value from props [props, number]. This state value shadows a value passed as a prop. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there.
error.derived-state-from-shadowed-props.ts:7:42
5 | const nothing = 0;
6 | const missDirection = number;
> 7 | const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this useState shadows props
8 |
9 | useEffect(() => {
10 | setDisplayValue(props.prefix + missDirection + nothing);
error.derived-state-from-shadowed-props.ts:7:42
5 | const nothing = 0;
6 | const missDirection = number;
> 7 | const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this useState shadows number
8 |
9 | useEffect(() => {
10 | setDisplayValue(props.prefix + missDirection + nothing);
error.derived-state-from-shadowed-props.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setDisplayValue(props.prefix + missDirection + nothing);
| ^^^^^^^^^^^^^^^ this setState synchronizes the state
11 | }, [props.prefix, missDirection, nothing]);
12 |
13 | return (
error.derived-state-from-shadowed-props.ts:16:8
14 | <div
15 | onClick={() => {
> 16 | setDisplayValue('clicked');
| ^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
17 | }}>
18 | {displayValue}
19 | </div>
```

View File

@@ -0,0 +1,21 @@
// @validateNoDerivedComputationsInEffects
import {useState, useEffect} from 'react';
function Component({props, number}) {
const nothing = 0;
const missDirection = number;
const [displayValue, setDisplayValue] = useState(props.prefix + missDirection + nothing);
useEffect(() => {
setDisplayValue(props.prefix + missDirection + nothing);
}, [props.prefix, missDirection, nothing]);
return (
<div
onClick={() => {
setDisplayValue('clicked');
}}>
{displayValue}
</div>
);
}

View File

@@ -0,0 +1,49 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [value]. Derived values 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.
error.derived-state-with-conditional.ts:9:6
7 | useEffect(() => {
8 | if (enabled) {
> 9 | setLocalValue(value);
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | } else {
11 | setLocalValue('disabled');
12 | }
```

View File

@@ -0,0 +1,21 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value, enabled}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
if (enabled) {
setLocalValue(value);
} else {
setLocalValue('disabled');
}
}, [value, enabled]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test', enabled: true}],
};

View File

@@ -0,0 +1,47 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [value]. Derived values 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.
error.derived-state-with-side-effects.ts:9:4
7 | useEffect(() => {
8 | console.log('Value changed:', value);
> 9 | setLocalValue(value);
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | document.title = `Value: ${value}`;
11 | }, [value]);
12 |
```

View File

@@ -0,0 +1,19 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({value}) {
const [localValue, setLocalValue] = useState('');
useEffect(() => {
console.log('Value changed:', value);
setLocalValue(value);
document.title = `Value: ${value}`;
}, [value]);
return <div>{localValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 'test'}],
};

View File

@@ -24,13 +24,15 @@ function BadExample() {
```
Found 1 error:
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)
Error: Derive values in render, not effects.
This setState() appears to derive a value from local state [firstName, lastName]. Derived values 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.
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ 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)
| ^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;

View File

@@ -0,0 +1,46 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [props, props, props]. Derived values 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.
error.invalid-derived-state-from-props-computed.ts:9:4
7 | useEffect(() => {
8 | const computed = props.prefix + props.value + props.suffix;
> 9 | setDisplayValue(computed);
| ^^^^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [props.prefix, props.value, props.suffix]);
11 |
12 | return <div>{displayValue}</div>;
```

View File

@@ -0,0 +1,18 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component(props) {
const [displayValue, setDisplayValue] = useState('');
useEffect(() => {
const computed = props.prefix + props.value + props.suffix;
setDisplayValue(computed);
}, [props.prefix, props.value, props.suffix]);
return <div>{displayValue}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prefix: '[', value: 'test', suffix: ']'}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({props}) {
const [fullName, setFullName] = useState(props.firstName + ' ' + props.lastName);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [props, props]. Derived values 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.
error.invalid-derived-state-from-props-destructured.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(props.firstName + ' ' + props.lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
9 | }, [props.firstName, props.lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,17 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({props}) {
const [fullName, setFullName] = useState(props.firstName + ' ' + props.lastName);
useEffect(() => {
setFullName(props.firstName + ' ' + props.lastName);
}, [props.firstName, props.lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};

View File

@@ -0,0 +1,45 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [firstName, lastName]. Derived values 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.
error.invalid-derived-state-from-props-in-effect.ts:8:4
6 |
7 | useEffect(() => {
> 8 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
9 | }, [firstName, lastName]);
10 |
11 | return <div>{fullName}</div>;
```

View File

@@ -0,0 +1,17 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{firstName: 'John', lastName: 'Doe'}],
};

View File

@@ -0,0 +1,43 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
export default function InProductLobbyGeminiCard(
input = 'empty',
) {
const [currInput, setCurrInput] = useState(input);
useEffect(() => {
setCurrInput(input)
}, [input]);
return (
<div>{currInput}</div>
)
}
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from props [input]. Derived values 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.
error.invalid-derived-state-from-props-with-default-value.ts:9:4
7 |
8 | useEffect(() => {
> 9 | setCurrInput(input)
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
10 | }, [input]);
11 |
12 | return (
```

View File

@@ -0,0 +1,15 @@
// @validateNoDerivedComputationsInEffects
export default function InProductLobbyGeminiCard(
input = 'empty',
) {
const [currInput, setCurrInput] = useState(input);
useEffect(() => {
setCurrInput(input)
}, [input]);
return (
<div>{currInput}</div>
)
}

View File

@@ -0,0 +1,53 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};
```
## Error
```
Found 1 error:
Error: Derive values in render, not effects.
This setState() appears to derive a value from local state [firstName, lastName]. Derived values 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.
error.invalid-derived-state-from-state-in-effect.ts:10:4
8 |
9 | useEffect(() => {
> 10 | setFullName(firstName + ' ' + lastName);
| ^^^^^^^^^^^ This should be computed during render, not in an effect
11 | }, [firstName, lastName]);
12 |
13 | return (
```

View File

@@ -0,0 +1,25 @@
// @validateNoDerivedComputationsInEffects
import {useEffect, useState} from 'react';
function Component() {
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return (
<div>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<div>{fullName}</div>
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
};

View File

@@ -0,0 +1,61 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function EndDate({startDate, endDate, onStartDateChange}) {
const [localStartDate, setLocalStartDate] = useState(startDate);
useEffect(() => {
setLocalStartDate(startDate);
}, [startDate]);
const onChange = (date) => {
setLocalStartDate(date);
onStartDateChange(date);
}
return <DateInput value={localStartDate} second={endDate} onChange={onChange} />
}
```
## Error
```
Found 1 error:
Error: Local state shadows parent state.
This setState() appears to derive a value from props [startDate]. This state value shadows a value passed as a prop. Instead of shadowing the prop with local state, hoist the state to the parent component and update it there.
error.shadowed-props-with-onchange.ts:4:47
2 |
3 | function EndDate({startDate, endDate, onStartDateChange}) {
> 4 | const [localStartDate, setLocalStartDate] = useState(startDate);
| ^^^^^^^^^^^^^^^^^^^ this useState shadows startDate
5 |
6 | useEffect(() => {
7 | setLocalStartDate(startDate);
error.shadowed-props-with-onchange.ts:7:8
5 |
6 | useEffect(() => {
> 7 | setLocalStartDate(startDate);
| ^^^^^^^^^^^^^^^^^ this setState synchronizes the state
8 | }, [startDate]);
9 |
10 | const onChange = (date) => {
error.shadowed-props-with-onchange.ts:11:8
9 |
10 | const onChange = (date) => {
> 11 | setLocalStartDate(date);
| ^^^^^^^^^^^^^^^^^ this setState updates the shadowed state, but should call an onChange event from the parent
12 | onStartDateChange(date);
13 | }
14 | return <DateInput value={localStartDate} second={endDate} onChange={onChange} />
```

View File

@@ -0,0 +1,15 @@
// @validateNoDerivedComputationsInEffects
function EndDate({startDate, endDate, onStartDateChange}) {
const [localStartDate, setLocalStartDate] = useState(startDate);
useEffect(() => {
setLocalStartDate(startDate);
}, [startDate]);
const onChange = (date) => {
setLocalStartDate(date);
onStartDateChange(date);
}
return <DateInput value={localStartDate} second={endDate} onChange={onChange} />
}

View File

@@ -0,0 +1,4 @@
yarn run v1.22.22
$ ./scripts/link-react-compiler-runtime.sh && yarn snap:ci
success Using linked package for "react-compiler-runtime".
$ yarn snap:build && yarn snap