Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e67b0e03e | ||
|
|
648c5045c4 | ||
|
|
04927c2ee7 | ||
|
|
1d9ccd1d1b |
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
|
||||
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
|
||||
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
@@ -275,6 +276,10 @@ function runWithEnvironment(
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoDerivedComputationsInEffects_exp) {
|
||||
validateNoDerivedComputationsInEffects_exp(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects) {
|
||||
env.logErrors(validateNoSetStateInEffects(hir, env));
|
||||
}
|
||||
|
||||
@@ -334,6 +334,12 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
|
||||
* during render. Generates a custom error message for each type of violation.
|
||||
*/
|
||||
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
|
||||
@@ -5,34 +5,21 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
Place,
|
||||
CallExpression,
|
||||
Instruction,
|
||||
isUseStateType,
|
||||
isUseRefType,
|
||||
GeneratedSource,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -58,257 +45,102 @@ type DerivationMetadata = {
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
|
||||
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
|
||||
|
||||
const effects: Array<HIRFunction> = [];
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
derivationCache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
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,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
}
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
addDerivationEntry(phi.place, sourcesIds, typeOfValue, derivationCache);
|
||||
}
|
||||
}
|
||||
for (const i of block.instructions) {
|
||||
function recordInstructiorDerivations(instr: Instruction): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructiorDerivations(instr);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
locals.set(lvalue.identifier.id, value.place.identifier.id);
|
||||
} else if (value.kind === 'ArrayExpression') {
|
||||
candidateDependencies.set(lvalue.identifier.id, value);
|
||||
} else if (value.kind === 'FunctionExpression') {
|
||||
functions.set(lvalue.identifier.id, value);
|
||||
} else if (
|
||||
value.kind === 'CallExpression' ||
|
||||
value.kind === 'MethodCall'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
const deps = candidateDependencies.get(value.args[1].identifier.id);
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
effectFunction != null &&
|
||||
deps != null &&
|
||||
deps.elements.length !== 0 &&
|
||||
deps.elements.every(element => element.kind === 'Identifier')
|
||||
) {
|
||||
const effectFunction = functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
effects.push(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier)) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
// Record setState usages everywhere
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall':
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (setStateCache.has(operand.loc.identifierName)) {
|
||||
setStateCache.get(operand.loc.identifierName)!.push(operand);
|
||||
} else {
|
||||
setStateCache.set(operand.loc.identifierName, [operand]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
const operandMetadata = derivationCache.get(operand.identifier.id);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
addDerivationEntry(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)) {
|
||||
addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
derivationCache,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
|
||||
CompilerError.invariant(dep.kind === 'Identifier', {
|
||||
reason: `Dependency is checked as a place above`,
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
loc: value.loc,
|
||||
message: 'this is checked as a place above',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
return locals.get(dep.identifier.id) ?? dep.identifier.id;
|
||||
});
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
recordInstructiorDerivations(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, errors, derivationCache, setStateCache);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = derivationCache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
derivationCache.set(derivedVar.identifier.id, newValue);
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
errors: CompilerError,
|
||||
derivationCache: Map<IdentifierId, DerivationMetadata>,
|
||||
setStateCache: Map<string | undefined | null, Array<Place>>,
|
||||
): void {
|
||||
const effectSetStateCache: Map<
|
||||
string | undefined | null,
|
||||
Array<Place>
|
||||
> = new Map();
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
|
||||
for (const dep of effectDeps) {
|
||||
values.set(dep, [dep]);
|
||||
}
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
loc: SourceLocation;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
typeOfValue: TypeOfValue;
|
||||
}> = [];
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
const setStateLocations: Array<SourceLocation> = [];
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
@@ -316,120 +148,91 @@ function validateEffect(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall':
|
||||
if (
|
||||
isSetStateType(operand.identifier) &&
|
||||
operand.loc !== GeneratedSource
|
||||
) {
|
||||
if (effectSetStateCache.has(operand.loc.identifierName)) {
|
||||
effectSetStateCache
|
||||
.get(operand.loc.identifierName)!
|
||||
.push(operand);
|
||||
} else {
|
||||
effectSetStateCache.set(operand.loc.identifierName, [operand]);
|
||||
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':
|
||||
case 'LoadGlobal': {
|
||||
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':
|
||||
case 'PropertyLoad':
|
||||
case 'BinaryExpression':
|
||||
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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const argMetadata = derivationCache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
loc: instr.value.callee.loc,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
typeOfValue: argMetadata.typeOfValue,
|
||||
});
|
||||
}
|
||||
} else if (instr.value.kind === 'CallExpression') {
|
||||
const calleeMetadata = derivationCache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
calleeMetadata !== undefined &&
|
||||
(calleeMetadata.typeOfValue === 'fromProps' ||
|
||||
calleeMetadata.typeOfValue === 'fromPropsAndState')
|
||||
) {
|
||||
// If the callee is a prop we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
|
||||
if (globals.has(instr.value.callee.identifier.id)) {
|
||||
// If the callee is a global we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
} else if (instr.value.kind === 'LoadGlobal') {
|
||||
globals.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
globals.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
if (values.has(operand.identifier.id)) {
|
||||
//
|
||||
return;
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
if (
|
||||
derivedSetStateCall.loc !== GeneratedSource &&
|
||||
effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
setStateCache.has(derivedSetStateCall.loc.identifierName) &&
|
||||
effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
|
||||
.length ===
|
||||
setStateCache.get(derivedSetStateCall.loc.identifierName)!.length
|
||||
) {
|
||||
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
|
||||
.map(sourceId => {
|
||||
const sourceMetadata = derivationCache.get(sourceId);
|
||||
return sourceMetadata?.place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
let description;
|
||||
|
||||
if (derivedSetStateCall.typeOfValue === 'fromProps') {
|
||||
description = `From props: [${derivedDepsStr}]`;
|
||||
} else if (derivedSetStateCall.typeOfValue === 'fromState') {
|
||||
description = `From local state: [${derivedDepsStr}]`;
|
||||
} else {
|
||||
description = `From props and local state: [${derivedDepsStr}]`;
|
||||
}
|
||||
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
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`,
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
}).withDetails({
|
||||
kind: 'error',
|
||||
loc: derivedSetStateCall.value.callee.loc,
|
||||
message: 'This should be computed during render, not in an effect',
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
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,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
FunctionExpression,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
Place,
|
||||
CallExpression,
|
||||
Instruction,
|
||||
isUseStateType,
|
||||
BasicBlock,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
|
||||
|
||||
type DerivationMetadata = {
|
||||
typeOfValue: TypeOfValue;
|
||||
place: Place;
|
||||
sourcesIds: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
type ValidationContext = {
|
||||
readonly functions: Map<IdentifierId, FunctionExpression>;
|
||||
readonly errors: CompilerError;
|
||||
readonly derivationCache: DerivationCache;
|
||||
readonly effects: Set<HIRFunction>;
|
||||
};
|
||||
|
||||
class DerivationCache {
|
||||
hasChanges: boolean = false;
|
||||
cache: Map<IdentifierId, DerivationMetadata> = new Map();
|
||||
|
||||
snapshot(): boolean {
|
||||
const hasChanges = this.hasChanges;
|
||||
this.hasChanges = false;
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
addDerivationEntry(
|
||||
derivedVar: Place,
|
||||
sourcesIds: Set<IdentifierId>,
|
||||
typeOfValue: TypeOfValue,
|
||||
): void {
|
||||
let newValue: DerivationMetadata = {
|
||||
place: derivedVar,
|
||||
sourcesIds: new Set(),
|
||||
typeOfValue: typeOfValue ?? 'ignored',
|
||||
};
|
||||
|
||||
if (sourcesIds !== undefined) {
|
||||
for (const id of sourcesIds) {
|
||||
const sourcePlace = this.cache.get(id)?.place;
|
||||
|
||||
if (sourcePlace === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the identifier of the source is a promoted identifier, then
|
||||
* we should set the target as the source.
|
||||
*/
|
||||
if (
|
||||
sourcePlace.identifier.name === null ||
|
||||
sourcePlace.identifier.name?.kind === 'promoted'
|
||||
) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
} else {
|
||||
newValue.sourcesIds.add(sourcePlace.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
const existingValue = this.cache.get(derivedVar.identifier.id);
|
||||
if (
|
||||
existingValue === undefined ||
|
||||
!this.isDerivationEqual(existingValue, newValue)
|
||||
) {
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
this.hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
private isDerivationEqual(
|
||||
a: DerivationMetadata,
|
||||
b: DerivationMetadata,
|
||||
): boolean {
|
||||
if (a.typeOfValue !== b.typeOfValue) {
|
||||
return false;
|
||||
}
|
||||
if (a.sourcesIds.size !== b.sourcesIds.size) {
|
||||
return false;
|
||||
}
|
||||
for (const id of a.sourcesIds) {
|
||||
if (!b.sourcesIds.has(id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
*
|
||||
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* // 🔴 Avoid: redundant state and unnecessary Effect
|
||||
* const [fullName, setFullName] = useState('');
|
||||
* useEffect(() => {
|
||||
* setFullName(firstName + ' ' + lastName);
|
||||
* }, [firstName, lastName]);
|
||||
* ```
|
||||
*
|
||||
* Instead use:
|
||||
*
|
||||
* ```
|
||||
* // ✅ Good: calculated during rendering
|
||||
* const fullName = firstName + ' ' + lastName;
|
||||
* ```
|
||||
*/
|
||||
export function validateNoDerivedComputationsInEffects_exp(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const derivationCache = new DerivationCache();
|
||||
const errors = new CompilerError();
|
||||
const effects: Set<HIRFunction> = new Set();
|
||||
|
||||
const context: ValidationContext = {
|
||||
functions,
|
||||
errors,
|
||||
derivationCache,
|
||||
effects,
|
||||
};
|
||||
|
||||
if (fn.fnType === 'Hook') {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
} else if (fn.fnType === 'Component') {
|
||||
const props = fn.params[0];
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
context.derivationCache.hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context);
|
||||
}
|
||||
}
|
||||
} while (context.derivationCache.snapshot());
|
||||
|
||||
for (const effect of effects) {
|
||||
validateEffect(effect, context);
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
function recordPhiDerivations(
|
||||
block: BasicBlock,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
for (const phi of block.phis) {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
let sourcesIds: Set<IdentifierId> = new Set();
|
||||
for (const operand of phi.operands.values()) {
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
sourcesIds.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue !== 'ignored') {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
phi.place,
|
||||
sourcesIds,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function joinValue(
|
||||
lvalueType: TypeOfValue,
|
||||
valueType: TypeOfValue,
|
||||
): TypeOfValue {
|
||||
if (lvalueType === 'ignored') return valueType;
|
||||
if (valueType === 'ignored') return lvalueType;
|
||||
if (lvalueType === valueType) return lvalueType;
|
||||
return 'fromPropsAndState';
|
||||
}
|
||||
|
||||
function recordInstructionDerivations(
|
||||
instr: Instruction,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
let typeOfValue: TypeOfValue = 'ignored';
|
||||
const sources: Set<IdentifierId> = new Set();
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context);
|
||||
}
|
||||
}
|
||||
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
if (
|
||||
isUseEffectHookType(callee.identifier) &&
|
||||
value.args.length === 2 &&
|
||||
value.args[0].kind === 'Identifier' &&
|
||||
value.args[1].kind === 'Identifier'
|
||||
) {
|
||||
const effectFunction = context.functions.get(value.args[0].identifier.id);
|
||||
if (effectFunction != null) {
|
||||
context.effects.add(effectFunction.loweredFunc.func);
|
||||
}
|
||||
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
|
||||
const stateValueSource = value.args[0];
|
||||
if (stateValueSource.kind === 'Identifier') {
|
||||
sources.add(stateValueSource.identifier.id);
|
||||
}
|
||||
typeOfValue = joinValue(typeOfValue, 'fromState');
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
const operandMetadata = context.derivationCache.cache.get(
|
||||
operand.identifier.id,
|
||||
);
|
||||
|
||||
if (operandMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
context.derivationCache.addDerivationEntry(lvalue, sources, typeOfValue);
|
||||
}
|
||||
|
||||
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)) {
|
||||
context.derivationCache.addDerivationEntry(
|
||||
operand,
|
||||
sources,
|
||||
typeOfValue,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Freeze:
|
||||
case Effect.Read: {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
details: [
|
||||
{
|
||||
kind: 'error',
|
||||
loc: operand.loc,
|
||||
message: 'Unexpected unknown effect',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
operand.effect,
|
||||
`Unexpected effect kind \`${operand.effect}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
context: ValidationContext,
|
||||
): void {
|
||||
const seenBlocks: Set<BlockId> = new Set();
|
||||
|
||||
const effectDerivedSetStateCalls: Array<{
|
||||
value: CallExpression;
|
||||
sourceIds: Set<IdentifierId>;
|
||||
}> = [];
|
||||
|
||||
const globals: Set<IdentifierId> = new Set();
|
||||
for (const block of effectFunction.body.blocks.values()) {
|
||||
for (const pred of block.preds) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// skip if block has a back edge
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
// Early return if any instruction is deriving a value from a ref
|
||||
if (isUseRefType(instr.lvalue.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
instr.value.kind === 'CallExpression' &&
|
||||
isSetStateType(instr.value.callee.identifier) &&
|
||||
instr.value.args.length === 1 &&
|
||||
instr.value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const argMetadata = context.derivationCache.cache.get(
|
||||
instr.value.args[0].identifier.id,
|
||||
);
|
||||
|
||||
if (argMetadata !== undefined) {
|
||||
effectDerivedSetStateCalls.push({
|
||||
value: instr.value,
|
||||
sourceIds: argMetadata.sourcesIds,
|
||||
});
|
||||
}
|
||||
} else if (instr.value.kind === 'CallExpression') {
|
||||
const calleeMetadata = context.derivationCache.cache.get(
|
||||
instr.value.callee.identifier.id,
|
||||
);
|
||||
|
||||
if (
|
||||
calleeMetadata !== undefined &&
|
||||
(calleeMetadata.typeOfValue === 'fromProps' ||
|
||||
calleeMetadata.typeOfValue === 'fromPropsAndState')
|
||||
) {
|
||||
// If the callee is a prop we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
|
||||
if (globals.has(instr.value.callee.identifier.id)) {
|
||||
// If the callee is a global we can't confidently say that it should be derived in render
|
||||
return;
|
||||
}
|
||||
} else if (instr.value.kind === 'LoadGlobal') {
|
||||
globals.add(instr.lvalue.identifier.id);
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
globals.add(operand.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
}
|
||||
|
||||
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
|
||||
context.errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
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,
|
||||
loc: derivedSetStateCall.value.callee.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [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;
|
||||
let t2;
|
||||
if ($[0] !== initialName) {
|
||||
t1 = () => {
|
||||
setName(initialName);
|
||||
};
|
||||
t2 = [initialName];
|
||||
$[0] = initialName;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
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>
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function MockComponent(t0) {
|
||||
const $ = _c(2);
|
||||
const { onSet } = t0;
|
||||
let t1;
|
||||
if ($[0] !== onSet) {
|
||||
t1 = <div onClick={() => onSet("clicked")}>Mock Component</div>;
|
||||
$[0] = onSet;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { propValue } = t0;
|
||||
const [, setValue] = useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== propValue) {
|
||||
t1 = () => {
|
||||
setValue(propValue);
|
||||
};
|
||||
t2 = [propValue];
|
||||
$[0] = propValue;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <MockComponent onSet={setValue} />;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ propValue: "test" }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>Mock Component</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue, onChange}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
@@ -32,15 +32,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.derived-state-conditionally-in-effect.ts:9:6
|
||||
7 | useEffect(() => {
|
||||
8 | if (enabled) {
|
||||
> 9 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^^^ 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)
|
||||
10 | } else {
|
||||
11 | setLocalValue('disabled');
|
||||
12 | }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value, enabled}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
@@ -29,15 +29,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.derived-state-from-default-props.ts:9:4
|
||||
7 |
|
||||
8 | useEffect(() => {
|
||||
> 9 | setCurrInput(input + localConst);
|
||||
| ^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^^ 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)
|
||||
10 | }, [input, localConst]);
|
||||
11 |
|
||||
12 | return <div>{currInput}</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({input = 'empty'}) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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.derived-state-from-local-state-in-effect.ts:10:6
|
||||
8 | useEffect(() => {
|
||||
9 | if (shouldChange) {
|
||||
> 10 | setCount(count + 1);
|
||||
| ^^^^^^^^ 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)
|
||||
11 | }
|
||||
12 | }, [count]);
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({shouldChange}) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldChange) {
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
@@ -36,15 +36,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props and 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.
|
||||
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.derived-state-from-prop-local-state-and-component-scope.ts:11:4
|
||||
9 |
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + middleName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ 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)
|
||||
12 | }, [firstName, middleName, lastName]);
|
||||
13 |
|
||||
14 | return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({firstName}) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{initialName: 'John'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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.derived-state-from-prop-setter-call-outside-effect-no-error.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setName(initialName);
|
||||
| ^^^^^^^ 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)
|
||||
9 | }, [initialName]);
|
||||
10 |
|
||||
11 | return (
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({initialName}) {
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
|
||||
}
|
||||
|
||||
function Component({propValue}) {
|
||||
const [value, setValue] = useState(null);
|
||||
useEffect(() => {
|
||||
setValue(propValue);
|
||||
}, [propValue]);
|
||||
|
||||
return <MockComponent onSet={setValue} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{propValue: 'test'}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 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.derived-state-from-prop-setter-used-outside-effect-no-error.ts:11:4
|
||||
9 | const [value, setValue] = useState(null);
|
||||
10 | useEffect(() => {
|
||||
> 11 | setValue(propValue);
|
||||
| ^^^^^^^^ 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)
|
||||
12 | }, [propValue]);
|
||||
13 |
|
||||
14 | return <MockComponent onSet={setValue} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function MockComponent({onSet}) {
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
@@ -29,15 +29,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.derived-state-from-prop-with-side-effect.ts:8:4
|
||||
6 |
|
||||
7 | useEffect(() => {
|
||||
> 8 | setLocalValue(value);
|
||||
| ^^^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^^^ 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)
|
||||
9 | document.title = `Value: ${value}`;
|
||||
10 | }, [value]);
|
||||
11 |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({value}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
@@ -33,15 +33,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.effect-contains-local-function-call.ts:12:4
|
||||
10 |
|
||||
11 | useEffect(() => {
|
||||
> 12 | setValue(propValue);
|
||||
| ^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^ 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)
|
||||
13 | localFunction();
|
||||
14 | }, [propValue]);
|
||||
15 |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component({propValue}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
@@ -31,15 +31,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: []) 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: 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.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ 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)
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
@@ -29,15 +29,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.invalid-derived-state-from-computed-props.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
|
||||
| ^^^^^^^^^^^^^^^ 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)
|
||||
10 | }, [props.prefix, props.value, props.suffix]);
|
||||
11 |
|
||||
12 | return <div>{displayValue}</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component(props) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
@@ -30,15 +30,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
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.
|
||||
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.invalid-derived-state-from-destructured-props.ts:10:4
|
||||
8 |
|
||||
9 | useEffect(() => {
|
||||
> 10 | setFullName(props.firstName + ' ' + props.lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ 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)
|
||||
11 | }, [props.firstName, props.lastName]);
|
||||
12 |
|
||||
13 | return <div>{fullName}</div>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function Component({props}) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
@@ -31,7 +31,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects_exp
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function Component(t0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateNoDerivedComputationsInEffects
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
|
||||
export default function Component({test}) {
|
||||
|
||||
@@ -26,15 +26,13 @@ function BadExample() {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: []) 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: 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.invalid-derived-computation-in-effect.ts:11:4
|
||||
9 | const [fullName, setFullName] = useState('');
|
||||
10 | useEffect(() => {
|
||||
> 11 | setFullName(firstName + ' ' + lastName);
|
||||
| ^^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
| ^^^^^^^^^^^ 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)
|
||||
12 | }, [firstName, lastName]);
|
||||
13 |
|
||||
14 | return <div>{fullName}</div>;
|
||||
|
||||
Reference in New Issue
Block a user