Compare commits
6 Commits
asserts-st
...
pr34446
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d651f69bc1 | ||
|
|
853550e7c8 | ||
|
|
5cf71b322d | ||
|
|
f807ce6492 | ||
|
|
7b38acca0b | ||
|
|
1d9c3927ea |
@@ -5,21 +5,223 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CompilerError, SourceLocation} from '..';
|
import {effect} from 'zod';
|
||||||
|
import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..';
|
||||||
import {ErrorCategory} from '../CompilerError';
|
import {ErrorCategory} from '../CompilerError';
|
||||||
import {
|
import {
|
||||||
ArrayExpression,
|
ArrayExpression,
|
||||||
|
BasicBlock,
|
||||||
BlockId,
|
BlockId,
|
||||||
|
Identifier,
|
||||||
FunctionExpression,
|
FunctionExpression,
|
||||||
HIRFunction,
|
HIRFunction,
|
||||||
IdentifierId,
|
IdentifierId,
|
||||||
|
Instruction,
|
||||||
|
Place,
|
||||||
isSetStateType,
|
isSetStateType,
|
||||||
isUseEffectHookType,
|
isUseEffectHookType,
|
||||||
|
isUseStateType,
|
||||||
|
IdentifierName,
|
||||||
|
GeneratedSource,
|
||||||
} from '../HIR';
|
} from '../HIR';
|
||||||
|
import {printInstruction} from '../HIR/PrintHIR';
|
||||||
import {
|
import {
|
||||||
eachInstructionValueOperand,
|
eachInstructionOperand,
|
||||||
eachTerminalOperand,
|
eachTerminalOperand,
|
||||||
|
eachInstructionLValue,
|
||||||
|
eachPatternOperand,
|
||||||
} from '../HIR/visitors';
|
} from '../HIR/visitors';
|
||||||
|
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||||
|
import {assertExhaustive} from '../Utils/utils';
|
||||||
|
|
||||||
|
// TODO: Maybe I can consolidate some types
|
||||||
|
type SetStateCall = {
|
||||||
|
loc: SourceLocation;
|
||||||
|
invalidDeps: DerivationMetadata;
|
||||||
|
setStateId: IdentifierId;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState';
|
||||||
|
|
||||||
|
type SetStateName = string | undefined | null;
|
||||||
|
|
||||||
|
type DerivationMetadata = {
|
||||||
|
typeOfValue: TypeOfValue;
|
||||||
|
// TODO: Rename to place
|
||||||
|
identifierPlace: Place;
|
||||||
|
sources: Place[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: This needs refining
|
||||||
|
type ErrorMetadata = {
|
||||||
|
errorType: TypeOfValue;
|
||||||
|
invalidDepInfo: string | undefined;
|
||||||
|
loc: SourceLocation;
|
||||||
|
setStateName: SetStateName;
|
||||||
|
};
|
||||||
|
|
||||||
|
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: DerivationMetadata[],
|
||||||
|
typeOfValue: TypeOfValue,
|
||||||
|
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||||
|
): void {
|
||||||
|
let newValue: DerivationMetadata = {
|
||||||
|
identifierPlace: target,
|
||||||
|
sources: [],
|
||||||
|
typeOfValue: typeOfValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
// If the identifier of the source is a promoted identifier, then
|
||||||
|
// we should set the target as the source.
|
||||||
|
if (source.identifierPlace.identifier.name?.kind === 'promoted') {
|
||||||
|
newValue.sources.push(target);
|
||||||
|
} else {
|
||||||
|
newValue.sources.push(...source.sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
derivedTuple.set(target.identifier.id, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInstr(
|
||||||
|
instr: Instruction,
|
||||||
|
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||||
|
setStateCalls: Map<SetStateName, Place[]>,
|
||||||
|
) {
|
||||||
|
// console.log(printInstruction(instr));
|
||||||
|
// console.log(instr);
|
||||||
|
let typeOfValue: TypeOfValue = 'ignored';
|
||||||
|
|
||||||
|
// TODO: Not sure if this will catch every time we create a new useState
|
||||||
|
if (
|
||||||
|
instr.value.kind === 'Destructure' &&
|
||||||
|
instr.value.lvalue.pattern.kind === 'ArrayPattern' &&
|
||||||
|
isUseStateType(instr.value.value.identifier)
|
||||||
|
) {
|
||||||
|
const value = instr.value.lvalue.pattern.items[0];
|
||||||
|
if (value.kind === 'Identifier') {
|
||||||
|
derivedTuple.set(value.identifier.id, {
|
||||||
|
identifierPlace: value,
|
||||||
|
sources: [value],
|
||||||
|
typeOfValue: 'fromState',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sources: DerivationMetadata[] = [];
|
||||||
|
for (const operand of eachInstructionOperand(instr)) {
|
||||||
|
const opSource = derivedTuple.get(operand.identifier.id);
|
||||||
|
if (opSource === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeOfValue = joinValue(typeOfValue, opSource.typeOfValue);
|
||||||
|
sources.push(opSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOfValue !== 'ignored') {
|
||||||
|
for (const lvalue of eachInstructionLValue(instr)) {
|
||||||
|
updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
derivedTuple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||||
|
) {
|
||||||
|
for (const phi of block.phis) {
|
||||||
|
for (const operand of phi.operands.values()) {
|
||||||
|
const source = derivedTuple.get(operand.identifier.id);
|
||||||
|
if (source !== undefined && source.typeOfValue === 'fromProps') {
|
||||||
|
if (
|
||||||
|
source.identifierPlace.identifier.name === null ||
|
||||||
|
source.identifierPlace.identifier.name?.kind === 'promoted'
|
||||||
|
) {
|
||||||
|
derivedTuple.set(phi.place.identifier.id, {
|
||||||
|
identifierPlace: phi.place,
|
||||||
|
sources: [phi.place],
|
||||||
|
typeOfValue: 'fromProps',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
derivedTuple.set(phi.place.identifier.id, {
|
||||||
|
identifierPlace: phi.place,
|
||||||
|
sources: source.sources,
|
||||||
|
typeOfValue: 'fromProps',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that useEffect is not used for derived computations which could/should
|
* Validates that useEffect is not used for derived computations which could/should
|
||||||
@@ -48,12 +250,54 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
|||||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||||
|
const derivedTuple: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||||
|
|
||||||
const errors = new CompilerError();
|
const effectSetStates: Map<SetStateName, Place[]> = new Map();
|
||||||
|
const setStateCalls: Map<SetStateName, Place[]> = new Map();
|
||||||
|
|
||||||
|
const errors: ErrorMetadata[] = [];
|
||||||
|
|
||||||
|
if (fn.fnType === 'Hook') {
|
||||||
|
for (const param of fn.params) {
|
||||||
|
if (param.kind === 'Identifier') {
|
||||||
|
derivedTuple.set(param.identifier.id, {
|
||||||
|
identifierPlace: param,
|
||||||
|
sources: [param],
|
||||||
|
typeOfValue: 'fromProps',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fn.fnType === 'Component') {
|
||||||
|
const props = fn.params[0];
|
||||||
|
if (props != null && props.kind === 'Identifier') {
|
||||||
|
derivedTuple.set(props.identifier.id, {
|
||||||
|
identifierPlace: props,
|
||||||
|
sources: [props],
|
||||||
|
typeOfValue: 'fromProps',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const block of fn.body.blocks.values()) {
|
for (const block of fn.body.blocks.values()) {
|
||||||
|
parseBlockPhi(block, derivedTuple);
|
||||||
|
|
||||||
for (const instr of block.instructions) {
|
for (const instr of block.instructions) {
|
||||||
const {lvalue, value} = instr;
|
const {lvalue, value} = instr;
|
||||||
|
|
||||||
|
parseInstr(instr, derivedTuple, setStateCalls);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Special case for function expressions, we need to parse nested instructions
|
||||||
|
* TODO: Can there be more recursive levels?
|
||||||
|
*/
|
||||||
|
if (value.kind === 'FunctionExpression') {
|
||||||
|
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||||
|
for (const instr of block.instructions) {
|
||||||
|
parseInstr(instr, derivedTuple, setStateCalls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (value.kind === 'LoadLocal') {
|
if (value.kind === 'LoadLocal') {
|
||||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||||
} else if (value.kind === 'ArrayExpression') {
|
} else if (value.kind === 'ArrayExpression') {
|
||||||
@@ -66,6 +310,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
|||||||
) {
|
) {
|
||||||
const callee =
|
const callee =
|
||||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isUseEffectHookType(callee.identifier) &&
|
isUseEffectHookType(callee.identifier) &&
|
||||||
value.args.length === 2 &&
|
value.args.length === 2 &&
|
||||||
@@ -97,6 +342,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
|||||||
validateEffect(
|
validateEffect(
|
||||||
effectFunction.loweredFunc.func,
|
effectFunction.loweredFunc.func,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
derivedTuple,
|
||||||
|
effectSetStates,
|
||||||
errors,
|
errors,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,43 +351,99 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errors.hasAnyErrors()) {
|
|
||||||
throw errors;
|
const throwableErrors = new CompilerError();
|
||||||
|
for (const error of errors) {
|
||||||
|
let reason;
|
||||||
|
let description = '';
|
||||||
|
// TODO: Not sure if this is robust enough.
|
||||||
|
/*
|
||||||
|
* 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.errorType !== 'fromState'
|
||||||
|
) {
|
||||||
|
reason =
|
||||||
|
'Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)';
|
||||||
|
} else {
|
||||||
|
reason =
|
||||||
|
'You may not need this effect. Values derived from 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)';
|
||||||
|
}
|
||||||
|
|
||||||
|
throwableErrors.push({
|
||||||
|
reason: reason,
|
||||||
|
description: `You are using invalid dependencies: \n\n${error.invalidDepInfo}`,
|
||||||
|
severity: ErrorSeverity.InvalidReact,
|
||||||
|
loc: error.loc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throwableErrors.hasAnyErrors()) {
|
||||||
|
throw throwableErrors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateEffect(
|
function validateEffect(
|
||||||
effectFunction: HIRFunction,
|
effectFunction: HIRFunction,
|
||||||
effectDeps: Array<IdentifierId>,
|
effectDeps: Array<IdentifierId>,
|
||||||
errors: CompilerError,
|
derivedTuple: Map<IdentifierId, DerivationMetadata>,
|
||||||
|
effectSetStates: Map<SetStateName, Place[]>,
|
||||||
|
errors: ErrorMetadata[],
|
||||||
): void {
|
): void {
|
||||||
|
/*
|
||||||
|
* TODO: This makes it so we only capture single line useEffects.
|
||||||
|
* We should be able to capture multiline as well
|
||||||
|
*/
|
||||||
for (const operand of effectFunction.context) {
|
for (const operand of effectFunction.context) {
|
||||||
if (isSetStateType(operand.identifier)) {
|
if (isSetStateType(operand.identifier)) {
|
||||||
continue;
|
continue;
|
||||||
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (derivedTuple.has(operand.identifier.id)) {
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// Captured something other than the effect dep or setState
|
// Captured something other than the effect dep or setState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This might be wrong gotta double check
|
||||||
|
let hasInvalidDep = false;
|
||||||
for (const dep of effectDeps) {
|
for (const dep of effectDeps) {
|
||||||
|
const depMetadata = derivedTuple.get(dep);
|
||||||
if (
|
if (
|
||||||
effectFunction.context.find(operand => operand.identifier.id === dep) ==
|
effectFunction.context.find(operand => operand.identifier.id === dep) !=
|
||||||
null
|
null ||
|
||||||
|
(depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored')
|
||||||
) {
|
) {
|
||||||
// effect dep wasn't actually used in the function
|
hasInvalidDep = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seenBlocks: Set<BlockId> = new Set();
|
if (!hasInvalidDep) {
|
||||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
console.log('early return 2');
|
||||||
for (const dep of effectDeps) {
|
// effect dep wasn't actually used in the function
|
||||||
values.set(dep, [dep]);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setStateLocations: Array<SourceLocation> = [];
|
const seenBlocks: Set<BlockId> = new Set();
|
||||||
|
// This variable is suspicious maybe we don't need it?
|
||||||
|
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||||
|
const effectInvalidlyDerived: Map<IdentifierId, DerivationMetadata> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
|
for (const dep of effectDeps) {
|
||||||
|
values.set(dep, [dep]);
|
||||||
|
const depMetadata = derivedTuple.get(dep);
|
||||||
|
if (depMetadata !== undefined) {
|
||||||
|
effectInvalidlyDerived.set(dep, depMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStateCallsInEffect: Array<SetStateCall> = [];
|
||||||
for (const block of effectFunction.body.blocks.values()) {
|
for (const block of effectFunction.body.blocks.values()) {
|
||||||
for (const pred of block.preds) {
|
for (const pred of block.preds) {
|
||||||
if (!seenBlocks.has(pred)) {
|
if (!seenBlocks.has(pred)) {
|
||||||
@@ -148,21 +451,29 @@ function validateEffect(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const phi of block.phis) {
|
|
||||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
parseBlockPhi(block, effectInvalidlyDerived);
|
||||||
for (const operand of phi.operands.values()) {
|
|
||||||
const deps = values.get(operand.identifier.id);
|
for (const instr of block.instructions) {
|
||||||
if (deps != null) {
|
if (
|
||||||
for (const dep of deps) {
|
instr.value.kind === 'CallExpression' &&
|
||||||
aggregateDeps.add(dep);
|
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) {
|
switch (instr.value.kind) {
|
||||||
case 'Primitive':
|
case 'Primitive':
|
||||||
case 'JSXText':
|
case 'JSXText':
|
||||||
@@ -183,7 +494,7 @@ function validateEffect(
|
|||||||
case 'CallExpression':
|
case 'CallExpression':
|
||||||
case 'MethodCall': {
|
case 'MethodCall': {
|
||||||
const aggregateDeps: Set<IdentifierId> = new Set();
|
const aggregateDeps: Set<IdentifierId> = new Set();
|
||||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
for (const operand of eachInstructionOperand(instr)) {
|
||||||
const deps = values.get(operand.identifier.id);
|
const deps = values.get(operand.identifier.id);
|
||||||
if (deps != null) {
|
if (deps != null) {
|
||||||
for (const dep of deps) {
|
for (const dep of deps) {
|
||||||
@@ -201,38 +512,63 @@ function validateEffect(
|
|||||||
instr.value.args.length === 1 &&
|
instr.value.args.length === 1 &&
|
||||||
instr.value.args[0].kind === 'Identifier'
|
instr.value.args[0].kind === 'Identifier'
|
||||||
) {
|
) {
|
||||||
const deps = values.get(instr.value.args[0].identifier.id);
|
const invalidDeps = derivedTuple.get(
|
||||||
if (deps != null && new Set(deps).size === effectDeps.length) {
|
instr.value.args[0].identifier.id,
|
||||||
setStateLocations.push(instr.value.callee.loc);
|
);
|
||||||
} else {
|
|
||||||
// doesn't depend on any deps
|
if (invalidDeps !== undefined) {
|
||||||
return;
|
setStateCallsInEffect.push({
|
||||||
|
loc: instr.value.callee.loc,
|
||||||
|
setStateId: instr.value.callee.identifier.id,
|
||||||
|
invalidDeps: invalidDeps,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
console.log('early return 4');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||||
if (values.has(operand.identifier.id)) {
|
if (values.has(operand.identifier.id)) {
|
||||||
//
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seenBlocks.add(block.id);
|
seenBlocks.add(block.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const loc of setStateLocations) {
|
for (const call of setStateCallsInEffect) {
|
||||||
|
const placeNames = call.invalidDeps.sources
|
||||||
|
.map(place => place.identifier.name?.value)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
let sourceNames = '';
|
||||||
|
let invalidDepInfo = '';
|
||||||
|
console.log(call.invalidDeps);
|
||||||
|
if (call.invalidDeps.typeOfValue === 'fromProps') {
|
||||||
|
sourceNames += `[${placeNames}], `;
|
||||||
|
sourceNames = sourceNames.slice(0, -2);
|
||||||
|
invalidDepInfo = sourceNames
|
||||||
|
? `Invalid deps from props ${sourceNames}`
|
||||||
|
: '';
|
||||||
|
} else if (call.invalidDeps.typeOfValue === 'fromState') {
|
||||||
|
sourceNames += `[${placeNames}], `;
|
||||||
|
sourceNames = sourceNames.slice(0, -2);
|
||||||
|
invalidDepInfo = sourceNames
|
||||||
|
? `Invalid deps from local state: ${sourceNames}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
errors.push({
|
errors.push({
|
||||||
category: ErrorCategory.EffectDerivationsOfState,
|
errorType: call.invalidDeps.typeOfValue,
|
||||||
reason:
|
invalidDepInfo: invalidDepInfo,
|
||||||
'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)',
|
loc: call.loc,
|
||||||
description: null,
|
setStateName:
|
||||||
loc,
|
call.loc !== GeneratedSource ? call.loc.identifierName : undefined,
|
||||||
suggestions: null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ function BadExample() {
|
|||||||
```
|
```
|
||||||
Found 1 error:
|
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: You may not need this effect. Values derived from 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 effect updates state based on other state values. Consider calculating this value directly during render.
|
||||||
|
|
||||||
error.invalid-derived-computation-in-effect.ts:9:4
|
error.invalid-derived-computation-in-effect.ts:9:4
|
||||||
7 | const [fullName, setFullName] = useState('');
|
7 | const [fullName, setFullName] = useState('');
|
||||||
8 | useEffect(() => {
|
8 | useEffect(() => {
|
||||||
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
|
> 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)
|
| ^^^^^^^^^^^ You may not need this effect. Values derived from 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)
|
||||||
10 | }, [firstName, lastName]);
|
10 | }, [firstName, lastName]);
|
||||||
11 |
|
11 |
|
||||||
12 | return <div>{fullName}</div>;
|
12 | return <div>{fullName}</div>;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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'}],
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
## 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}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function Component(t0) {
|
||||||
|
const $ = _c(6);
|
||||||
|
const { value, enabled } = t0;
|
||||||
|
const [localValue, setLocalValue] = useState("");
|
||||||
|
let t1;
|
||||||
|
let t2;
|
||||||
|
if ($[0] !== enabled || $[1] !== value) {
|
||||||
|
t1 = () => {
|
||||||
|
if (enabled) {
|
||||||
|
setLocalValue(value);
|
||||||
|
} else {
|
||||||
|
setLocalValue("disabled");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
t2 = [value, enabled];
|
||||||
|
$[0] = enabled;
|
||||||
|
$[1] = value;
|
||||||
|
$[2] = t1;
|
||||||
|
$[3] = t2;
|
||||||
|
} else {
|
||||||
|
t1 = $[2];
|
||||||
|
t2 = $[3];
|
||||||
|
}
|
||||||
|
useEffect(t1, t2);
|
||||||
|
let t3;
|
||||||
|
if ($[4] !== localValue) {
|
||||||
|
t3 = <div>{localValue}</div>;
|
||||||
|
$[4] = localValue;
|
||||||
|
$[5] = t3;
|
||||||
|
} else {
|
||||||
|
t3 = $[5];
|
||||||
|
}
|
||||||
|
return t3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{ value: "test", enabled: true }],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eval output
|
||||||
|
(kind: ok) <div>test</div>
|
||||||
@@ -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}],
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
## 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'}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function Component(t0) {
|
||||||
|
const $ = _c(5);
|
||||||
|
const { value } = t0;
|
||||||
|
const [localValue, setLocalValue] = useState("");
|
||||||
|
let t1;
|
||||||
|
let t2;
|
||||||
|
if ($[0] !== value) {
|
||||||
|
t1 = () => {
|
||||||
|
console.log("Value changed:", value);
|
||||||
|
setLocalValue(value);
|
||||||
|
document.title = `Value: ${value}`;
|
||||||
|
};
|
||||||
|
t2 = [value];
|
||||||
|
$[0] = value;
|
||||||
|
$[1] = t1;
|
||||||
|
$[2] = t2;
|
||||||
|
} else {
|
||||||
|
t1 = $[1];
|
||||||
|
t2 = $[2];
|
||||||
|
}
|
||||||
|
useEffect(t1, t2);
|
||||||
|
let t3;
|
||||||
|
if ($[3] !== localValue) {
|
||||||
|
t3 = <div>{localValue}</div>;
|
||||||
|
$[3] = localValue;
|
||||||
|
$[4] = t3;
|
||||||
|
} else {
|
||||||
|
t3 = $[4];
|
||||||
|
}
|
||||||
|
return t3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{ value: "test" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eval output
|
||||||
|
(kind: ok) <div>test</div>
|
||||||
|
logs: ['Value changed:','test']
|
||||||
@@ -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'}],
|
||||||
|
};
|
||||||
@@ -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: You may not need this effect. Values derived from 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 effect updates state based on other state values. Consider calculating this value directly during render.
|
||||||
|
|
||||||
|
error.bug-derived-state-from-mixed-deps.ts:9:4
|
||||||
|
7 |
|
||||||
|
8 | useEffect(() => {
|
||||||
|
> 9 | setDisplayName(prefix + name);
|
||||||
|
| ^^^^^^^^^^^^^^ You may not need this effect. Values derived from 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)
|
||||||
|
10 | }, [prefix, name]);
|
||||||
|
11 |
|
||||||
|
12 | return (
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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, '}],
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// @validateNoDerivedComputationsInEffects
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
function Component({user: {firstName, lastName}}) {
|
||||||
|
const [fullName, setFullName] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFullName(firstName + ' ' + lastName);
|
||||||
|
}, [firstName, lastName]);
|
||||||
|
|
||||||
|
return <div>{fullName}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{user: {firstName: 'John', lastName: 'Doe'}}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Error
|
||||||
|
|
||||||
|
```
|
||||||
|
Found 1 error:
|
||||||
|
|
||||||
|
Error: You may not need this effect. Values derived from 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 effect updates state based on other state values. Consider calculating this value directly during render.
|
||||||
|
|
||||||
|
error.invalid-derived-state-from-props-destructured.ts:8:4
|
||||||
|
6 |
|
||||||
|
7 | useEffect(() => {
|
||||||
|
> 8 | setFullName(firstName + ' ' + lastName);
|
||||||
|
| ^^^^^^^^^^^ You may not need this effect. Values derived from 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)
|
||||||
|
9 | }, [firstName, lastName]);
|
||||||
|
10 |
|
||||||
|
11 | return <div>{fullName}</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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'}],
|
||||||
|
};
|
||||||
@@ -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: You may not need this effect. Values derived from 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 effect updates state based on other state values. Consider calculating this value directly during render.
|
||||||
|
|
||||||
|
error.invalid-derived-state-from-props-in-effect.ts:8:4
|
||||||
|
6 |
|
||||||
|
7 | useEffect(() => {
|
||||||
|
> 8 | setFullName(firstName + ' ' + lastName);
|
||||||
|
| ^^^^^^^^^^^ You may not need this effect. Values derived from 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)
|
||||||
|
9 | }, [firstName, lastName]);
|
||||||
|
10 |
|
||||||
|
11 | return <div>{fullName}</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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'}],
|
||||||
|
};
|
||||||
@@ -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: You may not need this effect. Values derived from 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 effect updates state based on other state values. Consider calculating this value directly during render.
|
||||||
|
|
||||||
|
error.invalid-derived-state-from-state-in-effect.ts:10:4
|
||||||
|
8 |
|
||||||
|
9 | useEffect(() => {
|
||||||
|
> 10 | setFullName(firstName + ' ' + lastName);
|
||||||
|
| ^^^^^^^^^^^ You may not need this effect. Values derived from 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)
|
||||||
|
11 | }, [firstName, lastName]);
|
||||||
|
12 |
|
||||||
|
13 | return (
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
## 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: ']'}],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function Component(props) {
|
||||||
|
const $ = _c(7);
|
||||||
|
const [displayValue, setDisplayValue] = useState("");
|
||||||
|
let t0;
|
||||||
|
let t1;
|
||||||
|
if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) {
|
||||||
|
t0 = () => {
|
||||||
|
const computed = props.prefix + props.value + props.suffix;
|
||||||
|
setDisplayValue(computed);
|
||||||
|
};
|
||||||
|
t1 = [props.prefix, props.value, props.suffix];
|
||||||
|
$[0] = props.prefix;
|
||||||
|
$[1] = props.suffix;
|
||||||
|
$[2] = props.value;
|
||||||
|
$[3] = t0;
|
||||||
|
$[4] = t1;
|
||||||
|
} else {
|
||||||
|
t0 = $[3];
|
||||||
|
t1 = $[4];
|
||||||
|
}
|
||||||
|
useEffect(t0, t1);
|
||||||
|
let t2;
|
||||||
|
if ($[5] !== displayValue) {
|
||||||
|
t2 = <div>{displayValue}</div>;
|
||||||
|
$[5] = displayValue;
|
||||||
|
$[6] = t2;
|
||||||
|
} else {
|
||||||
|
t2 = $[6];
|
||||||
|
}
|
||||||
|
return t2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIXTURE_ENTRYPOINT = {
|
||||||
|
fn: Component,
|
||||||
|
params: [{ prefix: "[", value: "test", suffix: "]" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eval output
|
||||||
|
(kind: ok) <div>[test]</div>
|
||||||
@@ -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: ']'}],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user