mirror of
https://github.com/facebook/react.git
synced 2026-02-21 19:31:52 +00:00
[compiler] Extend setState in effect validation to useEffectEvent (#35214)
ValidateNoSetStateInEffects already supports transitive setter
functions. This PR marks any synchonous state setter useEffectEvent
function so we can validate that uEE isn't being used only as
misdirection to avoid the validation within an effect body.
The error points to the call of the effect event.
Example:
```js
export default function MyApp() {
const [count, setCount] = useState(0)
const effectEvent = useEffectEvent(() => {
setCount(10)
})
useEffect(() => {
effectEvent()
}, [])
return <div>{count}</div>;
```
```
Found 1 error:
Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
5 | })
6 | useEffect(() => {
> 7 | effectEvent()
| ^^^^^^^^^^^ Avoid calling setState() directly within an effect
8 | }, [])
9 | return <div>{count}</div>;
10 | }
```
This commit is contained in:
@@ -2023,6 +2023,11 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
|
||||
id.type.shapeId === 'BuiltInUseInsertionEffectHook'
|
||||
);
|
||||
}
|
||||
export function isUseEffectEventType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectEvent'
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseContextHookType(id: Identifier): boolean {
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IdentifierId,
|
||||
isSetStateType,
|
||||
isUseEffectHookType,
|
||||
isUseEffectEventType,
|
||||
isUseInsertionEffectHookType,
|
||||
isUseLayoutEffectHookType,
|
||||
isUseRefType,
|
||||
@@ -98,7 +99,20 @@ export function validateNoSetStateInEffects(
|
||||
instr.value.kind === 'MethodCall'
|
||||
? instr.value.receiver
|
||||
: instr.value.callee;
|
||||
if (
|
||||
|
||||
if (isUseEffectEventType(callee.identifier)) {
|
||||
const arg = instr.value.args[0];
|
||||
if (arg !== undefined && arg.kind === 'Identifier') {
|
||||
const setState = setStateFunctions.get(arg.identifier.id);
|
||||
if (setState !== undefined) {
|
||||
/**
|
||||
* This effect event function calls setState synchonously,
|
||||
* treat it as a setState function for transitive tracking
|
||||
*/
|
||||
setStateFunctions.set(instr.lvalue.identifier.id, setState);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
isUseEffectHookType(callee.identifier) ||
|
||||
isUseLayoutEffectHookType(callee.identifier) ||
|
||||
isUseInsertionEffectHookType(callee.identifier)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @loggerTestOnly @validateNoSetStateInEffects
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(true);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @loggerTestOnly @validateNoSetStateInEffects
|
||||
import { useEffect, useEffectEvent, useState } from "react";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(4);
|
||||
const [state, setState] = useState(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(true);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t0);
|
||||
let t1;
|
||||
if ($[1] !== effectEvent) {
|
||||
t1 = () => {
|
||||
effectEvent();
|
||||
};
|
||||
$[1] = effectEvent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"EffectSetState","reason":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":10,"column":4,"index":267},"end":{"line":10,"column":15,"index":278},"filename":"invalid-setState-in-useEffect-via-useEffectEvent.ts","identifierName":"effectEvent"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":108},"end":{"line":13,"column":1,"index":309},"filename":"invalid-setState-in-useEffect-via-useEffectEvent.ts"},"fnName":"Component","memoSlots":4,"memoBlocks":3,"memoValues":3,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,13 @@
|
||||
// @loggerTestOnly @validateNoSetStateInEffects
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(true);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEvent();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(10);
|
||||
});
|
||||
useEffect(() => {
|
||||
setTimeout(effectEvent, 10);
|
||||
});
|
||||
|
||||
const effectEventWithTimeout = useEffectEvent(() => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEventWithTimeout();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import { useEffect, useEffectEvent, useState } from "react";
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const $ = _c(7);
|
||||
const [state, setState] = useState(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
setState(10);
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t0);
|
||||
let t1;
|
||||
if ($[1] !== effectEvent) {
|
||||
t1 = () => {
|
||||
setTimeout(effectEvent, 10);
|
||||
};
|
||||
$[1] = effectEvent;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t1);
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
};
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const effectEventWithTimeout = useEffectEvent(t2);
|
||||
let t3;
|
||||
if ($[4] !== effectEventWithTimeout) {
|
||||
t3 = () => {
|
||||
effectEventWithTimeout();
|
||||
};
|
||||
$[4] = effectEventWithTimeout;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [];
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":164},"end":{"line":24,"column":1,"index":551},"filename":"valid-setState-in-useEffect-via-useEffectEvent-listener.ts"},"fnName":"Component","memoSlots":7,"memoBlocks":5,"memoValues":5,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) (0 , _react.useEffectEvent) is not a function
|
||||
@@ -0,0 +1,29 @@
|
||||
// @validateNoSetStateInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useEffect, useEffectEvent, useState} from 'react';
|
||||
|
||||
const shouldSetState = false;
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
setState(10);
|
||||
});
|
||||
useEffect(() => {
|
||||
setTimeout(effectEvent, 10);
|
||||
});
|
||||
|
||||
const effectEventWithTimeout = useEffectEvent(() => {
|
||||
setTimeout(() => {
|
||||
setState(20);
|
||||
}, 10);
|
||||
});
|
||||
useEffect(() => {
|
||||
effectEventWithTimeout();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useState, useRef, useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y}) {
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
const data = load({x, y});
|
||||
setData(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
}, [x, y]);
|
||||
|
||||
const effectEvent2 = useEffectEvent((xx, yy) => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX) || !areEqual(yy, previousY)) {
|
||||
const data = load({x: xx, y: yy});
|
||||
setData(data);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
effectEvent2(x, y);
|
||||
}, [x, y]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({x, y}) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 0, y: 0}],
|
||||
sequentialRenders: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 1, y: 1},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import { useState, useRef, useEffect, useEffectEvent } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(18);
|
||||
const { x, y } = t0;
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
let t1;
|
||||
if ($[0] !== x || $[1] !== y) {
|
||||
t1 = () => {
|
||||
const data_0 = load({ x, y });
|
||||
setData(data_0);
|
||||
};
|
||||
$[0] = x;
|
||||
$[1] = y;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const effectEvent = useEffectEvent(t1);
|
||||
let t2;
|
||||
if ($[3] !== effectEvent || $[4] !== x || $[5] !== y) {
|
||||
t2 = () => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
};
|
||||
$[3] = effectEvent;
|
||||
$[4] = x;
|
||||
$[5] = y;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
let t3;
|
||||
if ($[7] !== x || $[8] !== y) {
|
||||
t3 = [x, y];
|
||||
$[7] = x;
|
||||
$[8] = y;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = (xx, yy) => {
|
||||
const previousX_0 = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY_0 = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX_0) || !areEqual(yy, previousY_0)) {
|
||||
const data_1 = load({ x: xx, y: yy });
|
||||
setData(data_1);
|
||||
}
|
||||
};
|
||||
$[10] = t4;
|
||||
} else {
|
||||
t4 = $[10];
|
||||
}
|
||||
const effectEvent2 = useEffectEvent(t4);
|
||||
let t5;
|
||||
if ($[11] !== effectEvent2 || $[12] !== x || $[13] !== y) {
|
||||
t5 = () => {
|
||||
effectEvent2(x, y);
|
||||
};
|
||||
$[11] = effectEvent2;
|
||||
$[12] = x;
|
||||
$[13] = y;
|
||||
$[14] = t5;
|
||||
} else {
|
||||
t5 = $[14];
|
||||
}
|
||||
let t6;
|
||||
if ($[15] !== x || $[16] !== y) {
|
||||
t6 = [x, y];
|
||||
$[15] = x;
|
||||
$[16] = y;
|
||||
$[17] = t6;
|
||||
} else {
|
||||
t6 = $[17];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({ x, y }) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ x: 0, y: 0 }],
|
||||
sequentialRenders: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":179},"end":{"line":41,"column":1,"index":1116},"filename":"valid-setState-in-useEffect-via-useEffectEvent-with-ref.ts"},"fnName":"Component","memoSlots":18,"memoBlocks":6,"memoValues":6,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) [[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
[[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
[[ (exception in render) TypeError: (0 , _react.useEffectEvent) is not a function ]]
|
||||
@@ -0,0 +1,59 @@
|
||||
// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects @loggerTestOnly @compilationMode:"infer"
|
||||
import {useState, useRef, useEffect, useEffectEvent} from 'react';
|
||||
|
||||
function Component({x, y}) {
|
||||
const previousXRef = useRef(null);
|
||||
const previousYRef = useRef(null);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
const effectEvent = useEffectEvent(() => {
|
||||
const data = load({x, y});
|
||||
setData(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = x;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = y;
|
||||
if (!areEqual(x, previousX) || !areEqual(y, previousY)) {
|
||||
effectEvent();
|
||||
}
|
||||
}, [x, y]);
|
||||
|
||||
const effectEvent2 = useEffectEvent((xx, yy) => {
|
||||
const previousX = previousXRef.current;
|
||||
previousXRef.current = xx;
|
||||
const previousY = previousYRef.current;
|
||||
previousYRef.current = yy;
|
||||
if (!areEqual(xx, previousX) || !areEqual(yy, previousY)) {
|
||||
const data = load({x: xx, y: yy});
|
||||
setData(data);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
effectEvent2(x, y);
|
||||
}, [x, y]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function areEqual(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
function load({x, y}) {
|
||||
return x * y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{x: 0, y: 0}],
|
||||
sequentialRenders: [
|
||||
{x: 0, y: 0},
|
||||
{x: 1, y: 0},
|
||||
{x: 1, y: 1},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user