Skip to content

Commit 613cee9

Browse files
committed
[Compiler] Don't throw calculate in render if the blamed setter is used outside of the effect
Summary: If the setter is used both inside and outside the effect then usually the solution is more complex and requires hoisting state up to a parent component since we can't just remove the local state. To do this, we now have 2 caches that track setState usages (not just calls) since if the effect is passed as an argument or called outside the effect the solution gets more complex which we are trying to avoid for now
1 parent f305855 commit 613cee9

7 files changed

+235
-102
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
isUseStateType,
2121
BasicBlock,
2222
isUseRefType,
23+
GeneratedSource,
24+
SourceLocation,
2325
} from '../HIR';
2426
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
2527
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
@@ -36,6 +38,9 @@ type DerivationMetadata = {
3638
type ValidationContext = {
3739
readonly functions: Map<IdentifierId, FunctionExpression>;
3840
readonly derivationCache: Map<IdentifierId, DerivationMetadata>;
41+
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
42+
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
43+
readonly effects: Array<HIRFunction>;
3944
readonly errors: CompilerError;
4045
};
4146

@@ -69,9 +74,19 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
6974

7075
const errors = new CompilerError();
7176

77+
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
78+
const effectSetStateCache: Map<
79+
string | undefined | null,
80+
Array<Place>
81+
> = new Map();
82+
const effects: Array<HIRFunction> = [];
83+
7284
const context: ValidationContext = {
7385
functions,
7486
derivationCache,
87+
setStateCache,
88+
effectSetStateCache,
89+
effects,
7590
errors,
7691
};
7792

@@ -109,6 +124,10 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
109124
}
110125
}
111126

127+
for (const effect of effects) {
128+
validateEffect(effect, context);
129+
}
130+
112131
if (errors.hasAnyErrors()) {
113132
throw errors;
114133
}
@@ -228,7 +247,7 @@ function recordInstructionDerivations(
228247
) {
229248
const effectFunction = context.functions.get(value.args[0].identifier.id);
230249
if (effectFunction != null) {
231-
validateEffect(effectFunction.loweredFunc.func, context);
250+
context.effects.push(effectFunction.loweredFunc.func);
232251
}
233252
} else if (isUseStateType(lvalue.identifier) && value.args.length > 0) {
234253
const stateValueSource = value.args[0];
@@ -240,6 +259,14 @@ function recordInstructionDerivations(
240259
}
241260

242261
for (const operand of eachInstructionOperand(instr)) {
262+
if (isSetStateType(operand.identifier) && operand.loc !== GeneratedSource) {
263+
if (context.setStateCache.has(operand.loc.identifierName)) {
264+
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
265+
} else {
266+
context.setStateCache.set(operand.loc.identifierName, [operand]);
267+
}
268+
}
269+
243270
const operandMetadata = context.derivationCache.get(operand.identifier.id);
244271

245272
if (operandMetadata === undefined) {
@@ -316,6 +343,7 @@ function validateEffect(
316343

317344
const effectDerivedSetStateCalls: Array<{
318345
value: CallExpression;
346+
loc: SourceLocation;
319347
sourceIds: Set<IdentifierId>;
320348
}> = [];
321349

@@ -334,6 +362,23 @@ function validateEffect(
334362
return;
335363
}
336364

365+
for (const operand of eachInstructionOperand(instr)) {
366+
if (
367+
isSetStateType(operand.identifier) &&
368+
operand.loc !== GeneratedSource
369+
) {
370+
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
371+
context.effectSetStateCache
372+
.get(operand.loc.identifierName)!
373+
.push(operand);
374+
} else {
375+
context.effectSetStateCache.set(operand.loc.identifierName, [
376+
operand,
377+
]);
378+
}
379+
}
380+
}
381+
337382
if (
338383
instr.value.kind === 'CallExpression' &&
339384
isSetStateType(instr.value.callee.identifier) &&
@@ -347,6 +392,7 @@ function validateEffect(
347392
if (argMetadata !== undefined) {
348393
effectDerivedSetStateCalls.push({
349394
value: instr.value,
395+
loc: instr.value.callee.loc,
350396
sourceIds: argMetadata.sourcesIds,
351397
});
352398
}
@@ -379,13 +425,24 @@ function validateEffect(
379425
}
380426

381427
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
382-
context.errors.push({
383-
category: ErrorCategory.EffectDerivationsOfState,
384-
reason:
385-
'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)',
386-
description: null,
387-
loc: derivedSetStateCall.value.callee.loc,
388-
suggestions: null,
389-
});
428+
if (
429+
derivedSetStateCall.loc !== GeneratedSource &&
430+
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
431+
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
432+
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
433+
.length ===
434+
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
435+
.length -
436+
1
437+
) {
438+
context.errors.push({
439+
category: ErrorCategory.EffectDerivationsOfState,
440+
reason:
441+
'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)',
442+
description: null,
443+
loc: derivedSetStateCall.value.callee.loc,
444+
suggestions: null,
445+
});
446+
}
390447
}
391448
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoDerivedComputationsInEffects
6+
import {useEffect, useState} from 'react';
7+
8+
function Component({initialName}) {
9+
const [name, setName] = useState('');
10+
11+
useEffect(() => {
12+
setName(initialName);
13+
}, [initialName]);
14+
15+
return (
16+
<div>
17+
<input value={name} onChange={e => setName(e.target.value)} />
18+
</div>
19+
);
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: Component,
24+
params: [{initialName: 'John'}],
25+
};
26+
27+
```
28+
29+
## Code
30+
31+
```javascript
32+
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
33+
import { useEffect, useState } from "react";
34+
35+
function Component(t0) {
36+
const $ = _c(6);
37+
const { initialName } = t0;
38+
const [name, setName] = useState("");
39+
let t1;
40+
let t2;
41+
if ($[0] !== initialName) {
42+
t1 = () => {
43+
setName(initialName);
44+
};
45+
t2 = [initialName];
46+
$[0] = initialName;
47+
$[1] = t1;
48+
$[2] = t2;
49+
} else {
50+
t1 = $[1];
51+
t2 = $[2];
52+
}
53+
useEffect(t1, t2);
54+
let t3;
55+
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
56+
t3 = (e) => setName(e.target.value);
57+
$[3] = t3;
58+
} else {
59+
t3 = $[3];
60+
}
61+
let t4;
62+
if ($[4] !== name) {
63+
t4 = (
64+
<div>
65+
<input value={name} onChange={t3} />
66+
</div>
67+
);
68+
$[4] = name;
69+
$[5] = t4;
70+
} else {
71+
t4 = $[5];
72+
}
73+
return t4;
74+
}
75+
76+
export const FIXTURE_ENTRYPOINT = {
77+
fn: Component,
78+
params: [{ initialName: "John" }],
79+
};
80+
81+
```
82+
83+
### Eval output
84+
(kind: ok) <div><input value="John"></div>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoDerivedComputationsInEffects
6+
import {useEffect, useState} from 'react';
7+
8+
function MockComponent({onSet}) {
9+
return <div onClick={() => onSet('clicked')}>Mock Component</div>;
10+
}
11+
12+
function Component({propValue}) {
13+
const [value, setValue] = useState(null);
14+
useEffect(() => {
15+
setValue(propValue);
16+
}, [propValue]);
17+
18+
return <MockComponent onSet={setValue} />;
19+
}
20+
21+
export const FIXTURE_ENTRYPOINT = {
22+
fn: Component,
23+
params: [{propValue: 'test'}],
24+
};
25+
26+
```
27+
28+
## Code
29+
30+
```javascript
31+
import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects
32+
import { useEffect, useState } from "react";
33+
34+
function MockComponent(t0) {
35+
const $ = _c(2);
36+
const { onSet } = t0;
37+
let t1;
38+
if ($[0] !== onSet) {
39+
t1 = <div onClick={() => onSet("clicked")}>Mock Component</div>;
40+
$[0] = onSet;
41+
$[1] = t1;
42+
} else {
43+
t1 = $[1];
44+
}
45+
return t1;
46+
}
47+
48+
function Component(t0) {
49+
const $ = _c(4);
50+
const { propValue } = t0;
51+
const [, setValue] = useState(null);
52+
let t1;
53+
let t2;
54+
if ($[0] !== propValue) {
55+
t1 = () => {
56+
setValue(propValue);
57+
};
58+
t2 = [propValue];
59+
$[0] = propValue;
60+
$[1] = t1;
61+
$[2] = t2;
62+
} else {
63+
t1 = $[1];
64+
t2 = $[2];
65+
}
66+
useEffect(t1, t2);
67+
let t3;
68+
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
69+
t3 = <MockComponent onSet={setValue} />;
70+
$[3] = t3;
71+
} else {
72+
t3 = $[3];
73+
}
74+
return t3;
75+
}
76+
77+
export const FIXTURE_ENTRYPOINT = {
78+
fn: Component,
79+
params: [{ propValue: "test" }],
80+
};
81+
82+
```
83+
84+
### Eval output
85+
(kind: ok) <div>Mock Component</div>

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/effect-derived-computations/error.derived-state-from-prop-setter-call-outside-effect-no-error.expect.md

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)