Skip to content

Commit 5809638

Browse files
committed
docs: compare with set-state-in-effect
1 parent e2a1ff4 commit 5809638

File tree

3 files changed

+36
-11
lines changed

3 files changed

+36
-11
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# ESLint - React - You Might Not Need An Effect
22

3-
ESLint plugin to catch [unnecessary React `useEffect`s](https://react.dev/learn/you-might-not-need-an-effect) to make your code easier to follow, faster to run, and less error-prone. Highly recommended for new React developers as you learn its mental model, and even experienced developers may be surprised.
3+
ESLint plugin to catch [unnecessary React `useEffect`s](https://react.dev/learn/you-might-not-need-an-effect) to make your code easier to follow, faster to run, and less error-prone. Highly recommended for new React developers as you learn its mental model, and even experienced developers may be surprised!
4+
5+
The new [`eslint-plugin-react-hooks/set-state-in-effect`](https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect) flags synchronous `setState` calls inside effects, helping prevent unnecessary re-renders. However, unnecessary effects aren’t limited to synchronous `setState` calls. In contrast, this plugin:
6+
7+
1. Reports specific anti-patterns, providing actionable suggestions and links.
8+
2. Covers more than the documented cases.
9+
3. Analyzes props and refs — the other half of misusing React internals in effects.
10+
4. Considers effects' dependencies, since when the effect runs influences its impact.
11+
5. Incorporates advanced heuristics to minimize false negatives and false positives.
12+
6. Obsesses over unusual logic and syntax — because you never know what might end up in an effect.
413

514
## 📦 Installation
615

@@ -86,6 +95,20 @@ function Form() {
8695
}
8796
```
8897

98+
Disallow storing state derived from *any* state (even external) when the setter is only called once:
99+
100+
```js
101+
function Form() {
102+
const prefix = useQuery('/prefix');
103+
const [name, setName] = useState();
104+
const [prefixedName, setPrefixedName] = useState();
105+
106+
useEffect(() => {
107+
setPrefixedName(prefix + name)
108+
}, [prefix, name]);
109+
}
110+
```
111+
89112
### `no-chain-state-updates`[docs](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations)
90113

91114
Disallow chaining state updates in an effect:
@@ -176,7 +199,7 @@ function Child({ onDataFetched }) {
176199

177200
### `no-pass-ref-to-parent`[docs](https://react.dev/reference/react/forwardRef)
178201

179-
Disallow passing refs, or data from callbacks registered on them, to parents in an effect. Use `forwardRef` instead:
202+
Disallow passing refs to parents in an effect. Use `forwardRef` instead:
180203

181204
```js
182205
function Child({ onRef }) {
@@ -188,6 +211,8 @@ function Child({ onRef }) {
188211
}
189212
```
190213

214+
Disallow calling props inside callbacks registered on refs in an effect. Use `forwardRef` to register the callback in the parent instead.
215+
191216
```js
192217
const Child = ({ onClicked }) => {
193218
const ref = useRef();

src/util/ast.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,9 @@ export const getUseStateNode = (context, ref) => {
235235

236236
/**
237237
* Walks up the AST until a `useEffect` call, returning `false` if never found, or finds any of the following on the way:
238-
* - An async function
239-
* - A function declaration, which may be called at an arbitrary later time
238+
* - An `async` function
239+
* - A function declaration, which may be called at an arbitrary later time.
240+
* - While we return false for *this* call, we may still return true for a call to a function containing this call. Combined with `getUpstreamRefs()`, it will still flag calls to the containing function.
240241
* - A function passed as a callback to another function or `new` - event handler, `setTimeout`, `Promise.then()` `new ResizeObserver()`, etc.
241242
*
242243
* Otherwise returns `true`.
@@ -256,7 +257,6 @@ export const isImmediateCall = (node) => {
256257
// Obviously not immediate if async. I think this never occurs in isolation from the below conditions? But just in case for now.
257258
node.async ||
258259
// Inside a named or anonymous function that may be called later, either as a callback or by the developer.
259-
// Note while we return false for *this* call, we may still return true for a call to the function containing this call.
260260
node.type === "FunctionDeclaration" ||
261261
node.type === "FunctionExpression" ||
262262
node.type === "ArrowFunctionExpression"

test/no-derived-state.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -644,19 +644,19 @@ new MyRuleTester().run("no-derived-state", rule, {
644644
name: "From derived external state with single setter call",
645645
code: js`
646646
function Form() {
647-
const name = useQuery('/name');
648-
const [fullName, setFullName] = useState('');
647+
const prefix = useQuery('/prefix');
648+
const [name, setName] = useState();
649+
const [prefixedName, setPrefixedName] = useState();
649650
650651
useEffect(() => {
651-
const prefixedName = 'Dr. ' + name;
652-
setFullName(prefixedName)
653-
}, [name, setFullName]);
652+
setPrefixedName(prefix + name)
653+
}, [prefix, name]);
654654
}
655655
`,
656656
errors: [
657657
{
658658
messageId: "avoidSingleSetter",
659-
data: { state: "fullName" },
659+
data: { state: "prefixedName" },
660660
},
661661
],
662662
},

0 commit comments

Comments
 (0)