Skip to content

Commit 32ecad9

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

File tree

3 files changed

+42
-11
lines changed

3 files changed

+42
-11
lines changed

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
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.
4+
5+
Highly recommended for new React developers as you learn its mental model, and even experienced developers may be surprised!
6+
7+
## 🆚 Comparison
8+
9+
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. Often, this means you don’t need an effect at all!
10+
11+
However, unnecessary effects aren’t limited to synchronous `setState` calls. In contrast, `eslint-plugin-react-you-might-not-need-an-effect`:
12+
13+
1. Reports specific anti-patterns, providing actionable suggestions and links.
14+
2. Covers more than the documented [You Might Not Need An Effect](https://react.dev/learn/you-might-not-need-an-effect) cases.
15+
3. Analyzes props and refs — the other half of using React internals in effects.
16+
4. Considers effects' dependencies, since when the effect runs influences its impact.
17+
5. Incorporates advanced heuristics to minimize false negatives and false positives.
18+
6. Obsesses over unusual logic and syntax — because you never know what might end up in an effect.
419

520
## 📦 Installation
621

@@ -86,6 +101,20 @@ function Form() {
86101
}
87102
```
88103

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

91120
Disallow chaining state updates in an effect:
@@ -176,7 +205,7 @@ function Child({ onDataFetched }) {
176205

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

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

181210
```js
182211
function Child({ onRef }) {
@@ -188,6 +217,8 @@ function Child({ onRef }) {
188217
}
189218
```
190219

220+
Disallow calling props inside callbacks registered on refs in an effect. Use `forwardRef` to register the callback in the parent instead.
221+
191222
```js
192223
const Child = ({ onClicked }) => {
193224
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)