Skip to content

Commit fa76901

Browse files
authored
feat: add rule react-hooks/prefer-use-state-lazy-initialization, closes #214 (#218)
1 parent 7c01c35 commit fa76901

File tree

10 files changed

+464
-0
lines changed

10 files changed

+464
-0
lines changed

packages/ast/docs/README.md

+17
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- [getFunctionHeadLocation](README.md#getfunctionheadlocation)
3636
- [getFunctionIdentifier](README.md#getfunctionidentifier)
3737
- [getFunctionNameWithKind](README.md#getfunctionnamewithkind)
38+
- [getNestedCallExpressions](README.md#getnestedcallexpressions)
3839
- [getNestedIdentifiers](README.md#getnestedidentifiers)
3940
- [getNestedReturnStatements](README.md#getnestedreturnstatements)
4041
- [getPropertyName](README.md#getpropertyname)
@@ -394,6 +395,22 @@ Get the name and kind of a given function node.
394395

395396
---
396397

398+
### getNestedCallExpressions
399+
400+
**getNestedCallExpressions**(`node`): `TSESTree.CallExpression`[]
401+
402+
#### Parameters
403+
404+
| Name | Type |
405+
| :----- | :----- |
406+
| `node` | `Node` |
407+
408+
#### Returns
409+
410+
`TSESTree.CallExpression`[]
411+
412+
---
413+
397414
### getNestedIdentifiers
398415

399416
**getNestedIdentifiers**(`node`): `TSESTree.Identifier`[]

packages/ast/src/call.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { TSESTree } from "@typescript-eslint/types";
2+
3+
import { NodeType } from "./node-type";
4+
5+
export function getNestedCallExpressions(node: TSESTree.Node): TSESTree.CallExpression[] {
6+
const callExpressions: TSESTree.CallExpression[] = [];
7+
8+
if (node.type === NodeType.CallExpression) {
9+
callExpressions.push(node);
10+
}
11+
12+
if ("arguments" in node) {
13+
node.arguments.forEach((x) => {
14+
callExpressions.push(...getNestedCallExpressions(x));
15+
});
16+
}
17+
18+
if (
19+
"expression" in node
20+
&& node.expression !== true
21+
&& node.expression !== false
22+
) {
23+
callExpressions.push(...getNestedCallExpressions(node.expression));
24+
}
25+
26+
if ("left" in node) {
27+
callExpressions.push(...getNestedCallExpressions(node.left));
28+
}
29+
30+
if ("right" in node) {
31+
callExpressions.push(...getNestedCallExpressions(node.right));
32+
}
33+
34+
if ("test" in node && node.test !== null) {
35+
callExpressions.push(...getNestedCallExpressions(node.test));
36+
}
37+
38+
if ("consequent" in node) {
39+
Array.isArray(node.consequent)
40+
? node.consequent.forEach((x) => {
41+
callExpressions.push(...getNestedCallExpressions(x));
42+
})
43+
: callExpressions.push(...getNestedCallExpressions(node.consequent));
44+
}
45+
46+
if ("alternate" in node && node.alternate !== null) {
47+
Array.isArray(node.alternate)
48+
? node.alternate.forEach((x: TSESTree.Node) => {
49+
callExpressions.push(...getNestedCallExpressions(x));
50+
})
51+
: callExpressions.push(
52+
...getNestedCallExpressions(node.alternate),
53+
);
54+
}
55+
56+
if ("elements" in node) {
57+
node.elements.forEach((x) => {
58+
if (x !== null) {
59+
callExpressions.push(...getNestedCallExpressions(x));
60+
}
61+
});
62+
}
63+
64+
if ("properties" in node) {
65+
node.properties.forEach((x) => {
66+
callExpressions.push(...getNestedCallExpressions(x));
67+
});
68+
}
69+
70+
if ("expressions" in node) {
71+
node.expressions.forEach((x) => {
72+
callExpressions.push(...getNestedCallExpressions(x));
73+
});
74+
}
75+
76+
if (node.type === NodeType.Property) {
77+
callExpressions.push(...getNestedCallExpressions(node.value));
78+
}
79+
80+
if (node.type === NodeType.SpreadElement) {
81+
callExpressions.push(...getNestedCallExpressions(node.argument));
82+
}
83+
84+
if (node.type === NodeType.MemberExpression) {
85+
callExpressions.push(...getNestedCallExpressions(node.object));
86+
}
87+
88+
if (node.type === NodeType.UnaryExpression) {
89+
callExpressions.push(...getNestedCallExpressions(node.argument));
90+
}
91+
92+
if (node.type === NodeType.ChainExpression) {
93+
callExpressions.push(...getNestedCallExpressions(node.expression));
94+
}
95+
96+
if (node.type === NodeType.TSNonNullExpression) {
97+
callExpressions.push(...getNestedCallExpressions(node.expression));
98+
}
99+
100+
return callExpressions;
101+
}

packages/ast/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./assignment";
2+
export * from "./call";
23
export * from "./construction";
34
export * from "./equal";
45
export * from "./eslint-community-eslint-utils";

packages/eslint-plugin-react-hooks/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { name, version } from "../package.json";
55
import ensureCustomHooksUsingOtherHooks from "./rules/ensure-custom-hooks-using-other-hooks";
66
import ensureUseCallbackHasNonEmptyDeps from "./rules/ensure-use-callback-has-non-empty-deps";
77
import ensureUseMemoHasNonEmptyDeps from "./rules/ensure-use-memo-has-non-empty-deps";
8+
import preferUseStateLazyInitialization from "./rules/prefer-use-state-lazy-initialization";
89

910
export const meta = {
1011
name,
@@ -15,4 +16,5 @@ export const rules = {
1516
"ensure-custom-hooks-using-other-hooks": ensureCustomHooksUsingOtherHooks,
1617
"ensure-use-callback-has-non-empty-deps": ensureUseCallbackHasNonEmptyDeps,
1718
"ensure-use-memo-has-non-empty-deps": ensureUseMemoHasNonEmptyDeps,
19+
"prefer-use-state-lazy-initialization": preferUseStateLazyInitialization,
1820
} as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# react-hooks/prefer-use-state-lazy-initialization
2+
3+
## Rule category
4+
5+
Perf.
6+
7+
## What it does
8+
9+
Warns about function calls made inside `useState` calls.
10+
11+
## Why is this bad?
12+
13+
A function can be invoked inside a useState call to help create its initial state. However, subsequent renders will still invoke the function while discarding its return value. This is wasteful and can cause performance issues if the function call is expensive.
14+
15+
To combat this issue React allows useState calls to use an [initializer function](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state) which will only be called on the first render.
16+
17+
## Examples
18+
19+
### ❌ Incorrect
20+
21+
```tsx
22+
const [value, setValue] = useState(generateTodos());
23+
```
24+
25+
### ✅ Correct
26+
27+
```tsx
28+
const [value, setValue] = useState(() => generateTodos());
29+
```
30+
31+
## Further Reading
32+
33+
- [Official React documentation on useState](https://react.dev/reference/react/useState)

0 commit comments

Comments
 (0)