Skip to content

Commit 97cc560

Browse files
committed
[Compiler] Don't throw calculate in render if the blamed setter is used outside of the effect
1 parent c1a9e21 commit 97cc560

7 files changed

+245
-107
lines changed

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

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import {
1919
Instruction,
2020
isUseStateType,
2121
isUseRefType,
22+
GeneratedSource,
23+
SourceLocation,
2224
} from '../HIR';
23-
import {printInstruction} from '../HIR/PrintHIR';
2425
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
2526
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
2627
import {assertExhaustive} from '../Utils/utils';
@@ -60,6 +61,10 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
6061
const functions: Map<IdentifierId, FunctionExpression> = new Map();
6162

6263
const derivationCache: Map<IdentifierId, DerivationMetadata> = new Map();
64+
const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
65+
66+
const effects: Array<HIRFunction> = [];
67+
6368
if (fn.fnType === 'Hook') {
6469
for (const param of fn.params) {
6570
if (param.kind === 'Identifier') {
@@ -128,11 +133,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
128133
) {
129134
const effectFunction = functions.get(value.args[0].identifier.id);
130135
if (effectFunction != null) {
131-
validateEffect(
132-
effectFunction.loweredFunc.func,
133-
errors,
134-
derivationCache,
135-
);
136+
effects.push(effectFunction.loweredFunc.func);
136137
}
137138
} else if (isUseStateType(lvalue.identifier)) {
138139
const stateValueSource = value.args[0];
@@ -144,6 +145,25 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
144145
}
145146

146147
for (const operand of eachInstructionOperand(instr)) {
148+
// Record setState usages everywhere
149+
switch (instr.value.kind) {
150+
case 'JsxExpression':
151+
case 'CallExpression':
152+
case 'MethodCall':
153+
if (
154+
isSetStateType(operand.identifier) &&
155+
operand.loc !== GeneratedSource
156+
) {
157+
if (setStateCache.has(operand.loc.identifierName)) {
158+
setStateCache.get(operand.loc.identifierName)!.push(operand);
159+
} else {
160+
setStateCache.set(operand.loc.identifierName, [operand]);
161+
}
162+
}
163+
break;
164+
default:
165+
}
166+
147167
const operandMetadata = derivationCache.get(operand.identifier.id);
148168

149169
if (operandMetadata === undefined) {
@@ -212,6 +232,10 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
212232
}
213233
}
214234

235+
for (const effect of effects) {
236+
validateEffect(effect, errors, derivationCache, setStateCache);
237+
}
238+
215239
if (errors.hasAnyErrors()) {
216240
throw errors;
217241
}
@@ -269,11 +293,17 @@ function validateEffect(
269293
effectFunction: HIRFunction,
270294
errors: CompilerError,
271295
derivationCache: Map<IdentifierId, DerivationMetadata>,
296+
setStateCache: Map<string | undefined | null, Array<Place>>,
272297
): void {
298+
const effectSetStateCache: Map<
299+
string | undefined | null,
300+
Array<Place>
301+
> = new Map();
273302
const seenBlocks: Set<BlockId> = new Set();
274303

275304
const effectDerivedSetStateCalls: Array<{
276305
value: CallExpression;
306+
loc: SourceLocation;
277307
sourceIds: Set<IdentifierId>;
278308
}> = [];
279309

@@ -292,6 +322,28 @@ function validateEffect(
292322
return;
293323
}
294324

325+
for (const operand of eachInstructionOperand(instr)) {
326+
switch (instr.value.kind) {
327+
case 'JsxExpression':
328+
case 'CallExpression':
329+
case 'MethodCall':
330+
if (
331+
isSetStateType(operand.identifier) &&
332+
operand.loc !== GeneratedSource
333+
) {
334+
if (effectSetStateCache.has(operand.loc.identifierName)) {
335+
effectSetStateCache
336+
.get(operand.loc.identifierName)!
337+
.push(operand);
338+
} else {
339+
effectSetStateCache.set(operand.loc.identifierName, [operand]);
340+
}
341+
}
342+
break;
343+
default:
344+
}
345+
}
346+
295347
if (
296348
instr.value.kind === 'CallExpression' &&
297349
isSetStateType(instr.value.callee.identifier) &&
@@ -305,6 +357,7 @@ function validateEffect(
305357
if (argMetadata !== undefined) {
306358
effectDerivedSetStateCalls.push({
307359
value: instr.value,
360+
loc: instr.value.callee.loc,
308361
sourceIds: argMetadata.sourcesIds,
309362
});
310363
}
@@ -337,13 +390,22 @@ function validateEffect(
337390
}
338391

339392
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
340-
errors.push({
341-
category: ErrorCategory.EffectDerivationsOfState,
342-
reason:
343-
'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)',
344-
description: null,
345-
loc: derivedSetStateCall.value.callee.loc,
346-
suggestions: null,
347-
});
393+
if (
394+
derivedSetStateCall.loc !== GeneratedSource &&
395+
effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
396+
setStateCache.has(derivedSetStateCall.loc.identifierName) &&
397+
effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
398+
.length ===
399+
setStateCache.get(derivedSetStateCall.loc.identifierName)!.length
400+
) {
401+
errors.push({
402+
category: ErrorCategory.EffectDerivationsOfState,
403+
reason:
404+
'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)',
405+
description: null,
406+
loc: derivedSetStateCall.value.callee.loc,
407+
suggestions: null,
408+
});
409+
}
348410
}
349411
}
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)