Skip to content

Commit 5f9866a

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 e0814ac commit 5f9866a

File tree

5 files changed

+237
-10
lines changed

5 files changed

+237
-10
lines changed

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

Lines changed: 68 additions & 10 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';
@@ -38,6 +40,8 @@ type ValidationContext = {
3840
readonly errors: CompilerError;
3941
readonly derivationCache: DerivationCache;
4042
readonly effects: Set<HIRFunction>;
43+
readonly setStateCache: Map<string | undefined | null, Array<Place>>;
44+
readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
4145
};
4246

4347
class DerivationCache {
@@ -148,11 +152,19 @@ export function validateNoDerivedComputationsInEffects_exp(
148152
const errors = new CompilerError();
149153
const effects: Set<HIRFunction> = new Set();
150154

155+
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
156+
const effectSetStateCache: Map<
157+
string | undefined | null,
158+
Array<Place>
159+
> = new Map();
160+
151161
const context: ValidationContext = {
152162
functions,
153163
errors,
154164
derivationCache,
155165
effects,
166+
setStateCache,
167+
effectSetStateCache,
156168
};
157169

158170
if (fn.fnType === 'Hook') {
@@ -178,13 +190,16 @@ export function validateNoDerivedComputationsInEffects_exp(
178190
}
179191
}
180192

193+
let isFirstPass = true;
181194
do {
182195
for (const block of fn.body.blocks.values()) {
183196
recordPhiDerivations(block, context);
184197
for (const instr of block.instructions) {
185-
recordInstructionDerivations(instr, context);
198+
recordInstructionDerivations(instr, context, isFirstPass);
186199
}
187200
}
201+
202+
isFirstPass = false;
188203
} while (context.derivationCache.snapshot());
189204

190205
for (const effect of effects) {
@@ -239,6 +254,7 @@ function joinValue(
239254
function recordInstructionDerivations(
240255
instr: Instruction,
241256
context: ValidationContext,
257+
isFirstPass: boolean,
242258
): void {
243259
let typeOfValue: TypeOfValue = 'ignored';
244260
const sources: Set<IdentifierId> = new Set();
@@ -247,7 +263,7 @@ function recordInstructionDerivations(
247263
context.functions.set(lvalue.identifier.id, value);
248264
for (const [, block] of value.loweredFunc.func.body.blocks) {
249265
for (const instr of block.instructions) {
250-
recordInstructionDerivations(instr, context);
266+
recordInstructionDerivations(instr, context, isFirstPass);
251267
}
252268
}
253269
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
@@ -273,6 +289,18 @@ function recordInstructionDerivations(
273289
}
274290

275291
for (const operand of eachInstructionOperand(instr)) {
292+
if (
293+
isSetStateType(operand.identifier) &&
294+
operand.loc !== GeneratedSource &&
295+
isFirstPass
296+
) {
297+
if (context.setStateCache.has(operand.loc.identifierName)) {
298+
context.setStateCache.get(operand.loc.identifierName)!.push(operand);
299+
} else {
300+
context.setStateCache.set(operand.loc.identifierName, [operand]);
301+
}
302+
}
303+
276304
const operandMetadata = context.derivationCache.cache.get(
277305
operand.identifier.id,
278306
);
@@ -347,6 +375,7 @@ function validateEffect(
347375

348376
const effectDerivedSetStateCalls: Array<{
349377
value: CallExpression;
378+
loc: SourceLocation;
350379
sourceIds: Set<IdentifierId>;
351380
}> = [];
352381

@@ -365,6 +394,23 @@ function validateEffect(
365394
return;
366395
}
367396

397+
for (const operand of eachInstructionOperand(instr)) {
398+
if (
399+
isSetStateType(operand.identifier) &&
400+
operand.loc !== GeneratedSource
401+
) {
402+
if (context.effectSetStateCache.has(operand.loc.identifierName)) {
403+
context.effectSetStateCache
404+
.get(operand.loc.identifierName)!
405+
.push(operand);
406+
} else {
407+
context.effectSetStateCache.set(operand.loc.identifierName, [
408+
operand,
409+
]);
410+
}
411+
}
412+
}
413+
368414
if (
369415
instr.value.kind === 'CallExpression' &&
370416
isSetStateType(instr.value.callee.identifier) &&
@@ -378,6 +424,7 @@ function validateEffect(
378424
if (argMetadata !== undefined) {
379425
effectDerivedSetStateCalls.push({
380426
value: instr.value,
427+
loc: instr.value.callee.loc,
381428
sourceIds: argMetadata.sourcesIds,
382429
});
383430
}
@@ -410,13 +457,24 @@ function validateEffect(
410457
}
411458

412459
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
413-
context.errors.push({
414-
category: ErrorCategory.EffectDerivationsOfState,
415-
reason:
416-
'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)',
417-
description: null,
418-
loc: derivedSetStateCall.value.callee.loc,
419-
suggestions: null,
420-
});
460+
if (
461+
derivedSetStateCall.loc !== GeneratedSource &&
462+
context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
463+
context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
464+
context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
465+
.length ===
466+
context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
467+
.length -
468+
1
469+
) {
470+
context.errors.push({
471+
category: ErrorCategory.EffectDerivationsOfState,
472+
reason:
473+
'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)',
474+
description: null,
475+
loc: derivedSetStateCall.value.callee.loc,
476+
suggestions: null,
477+
});
478+
}
421479
}
422480
}
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_exp
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_exp
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_exp
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_exp
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>

0 commit comments

Comments
 (0)