mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[compiler] Fix false negatives and add data flow tree to compiler error for no-deriving-state-in-effects
Summary: Revamped the derivationCache graph. This fixes a bunch of bugs where sometimes we fail to track from which props/state we derived values from. Also, it is more intuitive and allows us to easily implement a Data Flow Tree. We can print this tree which gives insight on how the data is derived and should facilitate error resolution in complicated components Test Plan: Added a test case where we were failing to track derivations. Also updated the test cases with the new error containing the data flow tree
This commit is contained in:
+136
-44
@@ -102,31 +102,24 @@ class DerivationCache {
|
||||
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 (isNamedIdentifier(derivedVar)) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
}
|
||||
|
||||
if (newValue.sourcesIds.size === 0) {
|
||||
newValue.sourcesIds.add(derivedVar.identifier.id);
|
||||
for (const id of sourcesIds) {
|
||||
const sourceMetadata = this.cache.get(id);
|
||||
|
||||
if (sourceMetadata === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isNamedIdentifier(sourceMetadata.place)) {
|
||||
newValue.sourcesIds.add(sourceMetadata.place.identifier.id);
|
||||
} else {
|
||||
for (const sourcesSourceId of sourceMetadata.sourcesIds) {
|
||||
newValue.sourcesIds.add(sourcesSourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cache.set(derivedVar.identifier.id, newValue);
|
||||
@@ -151,6 +144,12 @@ class DerivationCache {
|
||||
}
|
||||
}
|
||||
|
||||
function isNamedIdentifier(place: Place): Boolean {
|
||||
return (
|
||||
place.identifier.name !== null && place.identifier.name?.kind === 'named'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
* be performed in render.
|
||||
@@ -202,7 +201,9 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (param.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(param.identifier.id, {
|
||||
place: param,
|
||||
sourcesIds: new Set([param.identifier.id]),
|
||||
sourcesIds: new Set(
|
||||
isNamedIdentifier(param) ? [param.identifier.id] : [],
|
||||
),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
@@ -212,7 +213,9 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
if (props != null && props.kind === 'Identifier') {
|
||||
context.derivationCache.cache.set(props.identifier.id, {
|
||||
place: props,
|
||||
sourcesIds: new Set([props.identifier.id]),
|
||||
sourcesIds: new Set(
|
||||
isNamedIdentifier(props) ? [props.identifier.id] : [],
|
||||
),
|
||||
typeOfValue: 'fromProps',
|
||||
});
|
||||
}
|
||||
@@ -223,10 +226,10 @@ export function validateNoDerivedComputationsInEffects_exp(
|
||||
context.derivationCache.takeSnapshot();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
recordPhiDerivations(block, context);
|
||||
}
|
||||
|
||||
context.derivationCache.checkForChanges();
|
||||
@@ -293,6 +296,7 @@ function recordInstructionDerivations(
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
context.functions.set(lvalue.identifier.id, value);
|
||||
for (const [, block] of value.loweredFunc.func.body.blocks) {
|
||||
recordPhiDerivations(block, context);
|
||||
for (const instr of block.instructions) {
|
||||
recordInstructionDerivations(instr, context, isFirstPass);
|
||||
}
|
||||
@@ -341,9 +345,7 @@ function recordInstructionDerivations(
|
||||
}
|
||||
|
||||
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
|
||||
for (const id of operandMetadata.sourcesIds) {
|
||||
sources.add(id);
|
||||
}
|
||||
sources.add(operand.identifier.id);
|
||||
}
|
||||
|
||||
if (typeOfValue === 'ignored') {
|
||||
@@ -406,6 +408,60 @@ function recordInstructionDerivations(
|
||||
}
|
||||
}
|
||||
|
||||
function buildDataFlowTree(
|
||||
sourceId: IdentifierId,
|
||||
indent: string = '',
|
||||
isLast: boolean = true,
|
||||
context: ValidationContext,
|
||||
): string {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
if (!sourceMetadata || !sourceMetadata.place.identifier.name?.value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceName = sourceMetadata.place.identifier.name.value;
|
||||
const prefix = indent + (isLast ? '└── ' : '├── ');
|
||||
const childIndent = indent + (isLast ? ' ' : '│ ');
|
||||
|
||||
const childSourceIds = Array.from(sourceMetadata.sourcesIds).filter(
|
||||
id => id !== sourceId,
|
||||
);
|
||||
|
||||
const isOriginal = childSourceIds.length === 0;
|
||||
|
||||
let result = `${prefix}${sourceName}`;
|
||||
|
||||
if (isOriginal) {
|
||||
let typeLabel: string;
|
||||
if (sourceMetadata.typeOfValue === 'fromProps') {
|
||||
typeLabel = 'Prop';
|
||||
} else if (sourceMetadata.typeOfValue === 'fromState') {
|
||||
typeLabel = 'State';
|
||||
} else {
|
||||
typeLabel = 'Prop and State';
|
||||
}
|
||||
result += ` (${typeLabel})`;
|
||||
}
|
||||
|
||||
if (childSourceIds.length > 0) {
|
||||
result += '\n';
|
||||
childSourceIds.forEach((childId, index) => {
|
||||
const childTree = buildDataFlowTree(
|
||||
childId,
|
||||
childIndent,
|
||||
index === childSourceIds.length - 1,
|
||||
context,
|
||||
);
|
||||
if (childTree) {
|
||||
result += childTree + '\n';
|
||||
}
|
||||
});
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
context: ValidationContext,
|
||||
@@ -508,27 +564,63 @@ function validateEffect(
|
||||
.length -
|
||||
1
|
||||
) {
|
||||
const derivedDepsStr = Array.from(derivedSetStateCall.sourceIds)
|
||||
.map(sourceId => {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
return sourceMetadata?.place.identifier.name?.value;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const allSourceIds = Array.from(derivedSetStateCall.sourceIds);
|
||||
const trees = allSourceIds
|
||||
.map((id, index) =>
|
||||
buildDataFlowTree(id, '', index === allSourceIds.length - 1, context),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
let description;
|
||||
const propsSet = new Set<string>();
|
||||
const stateSet = new Set<string>();
|
||||
|
||||
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}]`;
|
||||
for (const sourceId of derivedSetStateCall.sourceIds) {
|
||||
const sourceMetadata = context.derivationCache.cache.get(sourceId);
|
||||
if (
|
||||
sourceMetadata &&
|
||||
sourceMetadata.place.identifier.name?.value &&
|
||||
(sourceMetadata.sourcesIds.size === 0 ||
|
||||
(sourceMetadata.sourcesIds.size === 1 &&
|
||||
sourceMetadata.sourcesIds.has(sourceId)))
|
||||
) {
|
||||
const name = sourceMetadata.place.identifier.name.value;
|
||||
if (sourceMetadata.typeOfValue === 'fromProps') {
|
||||
propsSet.add(name);
|
||||
} else if (sourceMetadata.typeOfValue === 'fromState') {
|
||||
stateSet.add(name);
|
||||
} else if (sourceMetadata.typeOfValue === 'fromPropsAndState') {
|
||||
propsSet.add(name);
|
||||
stateSet.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const propsArr = Array.from(propsSet);
|
||||
const stateArr = Array.from(stateSet);
|
||||
|
||||
let rootSources = '';
|
||||
if (propsArr.length > 0) {
|
||||
rootSources += `Props: [${propsArr.join(', ')}]`;
|
||||
}
|
||||
if (stateArr.length > 0) {
|
||||
if (rootSources) rootSources += '\n';
|
||||
rootSources += `State: [${stateArr.join(', ')}]`;
|
||||
}
|
||||
|
||||
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
${rootSources}
|
||||
|
||||
Data Flow Tree:
|
||||
${trees.join('\n')}
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
|
||||
|
||||
context.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`,
|
||||
description: description,
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'You might not need an effect. Derive values in render, not effects.',
|
||||
|
||||
+10
-1
@@ -34,7 +34,16 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: 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(() => {
|
||||
|
||||
+10
-1
@@ -31,7 +31,16 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [input]
|
||||
|
||||
Data Flow Tree:
|
||||
└── input (Prop)
|
||||
|
||||
See: 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 |
|
||||
|
||||
+10
-1
@@ -28,7 +28,16 @@ Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [count]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
State: [count]
|
||||
|
||||
Data Flow Tree:
|
||||
└── count (State)
|
||||
|
||||
See: 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(() => {
|
||||
|
||||
+12
-1
@@ -38,7 +38,18 @@ Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From props and local state: [firstName, lastName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [firstName]
|
||||
State: [lastName]
|
||||
|
||||
Data Flow Tree:
|
||||
├── firstName (Prop)
|
||||
└── lastName (State)
|
||||
|
||||
See: 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 |
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({ value }) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div>{checked}</div>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state.
|
||||
|
||||
error.derived-state-from-prop-setter-ternary.ts:7:4
|
||||
5 |
|
||||
6 | useEffect(() => {
|
||||
> 7 | setChecked(value === '' ? [] : value.split(','));
|
||||
| ^^^^^^^^^^ This should be computed during render, not in an effect
|
||||
8 | }, [value]);
|
||||
9 |
|
||||
10 | return (
|
||||
```
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// @validateNoDerivedComputationsInEffects_exp
|
||||
|
||||
function Component({ value }) {
|
||||
const [checked, setChecked] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(value === '' ? [] : value.split(','));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div>{checked}</div>
|
||||
)
|
||||
}
|
||||
+10
-1
@@ -31,7 +31,16 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [value]
|
||||
|
||||
Data Flow Tree:
|
||||
└── value (Prop)
|
||||
|
||||
See: 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 |
|
||||
|
||||
+10
-1
@@ -35,7 +35,16 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [propValue]
|
||||
|
||||
Data Flow Tree:
|
||||
└── propValue (Prop)
|
||||
|
||||
See: 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 |
|
||||
|
||||
+10
-1
@@ -33,7 +33,16 @@ Found 1 error:
|
||||
|
||||
Error: You might not need an effect. Derive values in render, not effects.
|
||||
|
||||
Derived values (From local state: [firstName]) should be computed during render, rather than in effects. Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
State: [firstName]
|
||||
|
||||
Data Flow Tree:
|
||||
└── firstName (State)
|
||||
|
||||
See: 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('');
|
||||
|
||||
+11
-1
@@ -31,7 +31,17 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
|
||||
|
||||
Data Flow Tree:
|
||||
└── computed
|
||||
└── props (Prop)
|
||||
|
||||
See: 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(() => {
|
||||
|
||||
+10
-1
@@ -32,7 +32,16 @@ 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.
|
||||
Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
|
||||
|
||||
This setState call is setting a derived value that depends on the following reactive sources:
|
||||
|
||||
Props: [props]
|
||||
|
||||
Data Flow Tree:
|
||||
└── props (Prop)
|
||||
|
||||
See: 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 |
|
||||
|
||||
Reference in New Issue
Block a user