Skip to content

Commit 2d86f3c

Browse files
authored
fix: enhance hooks-extra/prefer-use-state-lazy-initialization to correctly detect other hooks called within useState(...) (#1006)
1 parent 742411e commit 2d86f3c

File tree

4 files changed

+85
-146
lines changed

4 files changed

+85
-146
lines changed

packages/core/src/hook/hook-name.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ export const RE_HOOK_NAME = /^use[A-Z\d]/u;
33
export function isReactHookName(name: string) {
44
return name === "use" || RE_HOOK_NAME.test(name);
55
}
6+
7+
export function isReactHookNameLoose(name: string) {
8+
return name.startsWith("use");
9+
}

packages/core/src/hook/is.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export function isReactHookCall(node: TSESTree.Node | _) {
3434
return false;
3535
}
3636

37-
export function isReactHookCallWithName(context: RuleContext, node: TSESTree.CallExpression | _) {
38-
if (node == null) return constFalse;
37+
export function isReactHookCallWithName(context: RuleContext, node: TSESTree.Node | _) {
38+
if (node == null || node.type !== T.CallExpression) return constFalse;
3939
const {
4040
importSource = DEFAULT_ESLINT_REACT_SETTINGS.importSource,
4141
skipImportCheck = true,
@@ -57,8 +57,8 @@ export function isReactHookCallWithName(context: RuleContext, node: TSESTree.Cal
5757
};
5858
}
5959

60-
export function isReactHookCallWithNameLoose(node: TSESTree.CallExpression | _) {
61-
if (node == null) return constFalse;
60+
export function isReactHookCallWithNameLoose(node: TSESTree.Node | _) {
61+
if (node == null || node.type !== T.CallExpression) return constFalse;
6262
return (name: string) => {
6363
switch (node.callee.type) {
6464
case T.Identifier:
@@ -109,16 +109,20 @@ export function isUseEffectCallLoose(node: TSESTree.Node | _) {
109109
}
110110
}
111111

112+
export const isUseCall = flip(isReactHookCallWithName)("use");
113+
export const isUseActionStateCall = flip(isReactHookCallWithName)("useActionState");
112114
export const isUseCallbackCall = flip(isReactHookCallWithName)("useCallback");
113115
export const isUseContextCall = flip(isReactHookCallWithName)("useContext");
114116
export const isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue");
115117
export const isUseDeferredValueCall = flip(isReactHookCallWithName)("useDeferredValue");
116118
export const isUseEffectCall = flip(isReactHookCallWithName)("useEffect");
119+
export const isUseFormStatusCall = flip(isReactHookCallWithName)("useFormStatus");
117120
export const isUseIdCall = flip(isReactHookCallWithName)("useId");
118121
export const isUseImperativeHandleCall = flip(isReactHookCallWithName)("useImperativeHandle");
119122
export const isUseInsertionEffectCall = flip(isReactHookCallWithName)("useInsertionEffect");
120123
export const isUseLayoutEffectCall = flip(isReactHookCallWithName)("useLayoutEffect");
121124
export const isUseMemoCall = flip(isReactHookCallWithName)("useMemo");
125+
export const isUseOptimisticCall = flip(isReactHookCallWithName)("useOptimistic");
122126
export const isUseReducerCall = flip(isReactHookCallWithName)("useReducer");
123127
export const isUseRefCall = flip(isReactHookCallWithName)("useRef");
124128
export const isUseStateCall = flip(isReactHookCallWithName)("useState");

packages/plugins/eslint-plugin-react-hooks-extra/src/rules/prefer-use-state-lazy-initialization.spec.ts

+48-121
Original file line numberDiff line numberDiff line change
@@ -5,178 +5,145 @@ import { allValid, ruleTester } from "../../../../../test";
55
import rule, { RULE_NAME } from "./prefer-use-state-lazy-initialization";
66

77
ruleTester.run(RULE_NAME, rule, {
8-
invalid: ([
9-
["getValue()", T.CallExpression],
10-
["getValue(1, 2, 3)", T.CallExpression],
11-
["new Foo()", T.NewExpression],
12-
] satisfies [string, T][]).flatMap(([expression, type]) => [
8+
invalid: [
139
{
14-
code: `import { useState } from "react"; useState(1 || ${expression})`,
10+
code: `import { useState } from "react"; useState(1 || getValue())`,
1511
errors: [
1612
{
17-
type: T.LogicalExpression,
13+
type: T.CallExpression,
1814
messageId: "preferUseStateLazyInitialization",
1915
},
2016
],
2117
},
2218
{
23-
code: `import { useState } from "react"; useState(2 < ${expression})`,
19+
code: `import { useState } from "react"; useState(2 < getValue())`,
2420
errors: [
2521
{
26-
type: T.BinaryExpression,
22+
type: T.CallExpression,
2723
messageId: "preferUseStateLazyInitialization",
2824
},
2925
],
3026
},
3127
{
32-
code: `import { useState } from "react"; useState(${expression})`,
28+
code: `import { useState } from "react"; useState(1 < 2 ? getValue() : 4)`,
3329
errors: [
3430
{
35-
type,
31+
type: T.CallExpression,
3632
messageId: "preferUseStateLazyInitialization",
3733
},
3834
],
3935
},
4036
{
41-
code: `import { useState } from "react"; useState(a ? b : ${expression})`,
37+
code: `import { useState } from "react"; useState(a ? b : getValue())`,
4238
errors: [
4339
{
44-
type: T.ConditionalExpression,
40+
type: T.CallExpression,
4541
messageId: "preferUseStateLazyInitialization",
4642
},
4743
],
4844
},
4945
{
50-
code: `import { useState } from "react"; useState(${expression} ? b : c)`,
46+
code: `import { useState } from "react"; useState(getValue() ? b : c)`,
5147
errors: [
5248
{
53-
type: T.ConditionalExpression,
49+
type: T.CallExpression,
5450
messageId: "preferUseStateLazyInitialization",
5551
},
5652
],
5753
},
5854
{
59-
code: `import { useState } from "react"; useState(a ? (b ? ${expression} : b2) : c)`,
55+
code: `import { useState } from "react"; useState(a ? (b ? getValue() : b2) : c)`,
6056
errors: [
6157
{
62-
type: T.ConditionalExpression,
58+
type: T.CallExpression,
6359
messageId: "preferUseStateLazyInitialization",
6460
},
6561
],
6662
},
6763
{
68-
code: `import { useState } from "react"; useState(${expression} && b)`,
64+
code: `import { useState } from "react"; useState(getValue() && b)`,
6965
errors: [
7066
{
71-
type: T.LogicalExpression,
67+
type: T.CallExpression,
7268
messageId: "preferUseStateLazyInitialization",
7369
},
7470
],
7571
},
7672
{
77-
code: `import { useState } from "react"; useState(a && ${expression})`,
73+
code: `import { useState } from "react"; useState(a() && new Foo())`,
7874
errors: [
7975
{
80-
type: T.LogicalExpression,
76+
type: T.CallExpression,
8177
messageId: "preferUseStateLazyInitialization",
8278
},
83-
],
84-
},
85-
{
86-
code: `import { useState } from "react"; useState(${expression} && b())`,
87-
errors: [
88-
{
89-
type: T.LogicalExpression,
90-
messageId: "preferUseStateLazyInitialization",
91-
},
92-
],
93-
},
94-
{
95-
code: `import { useState } from "react"; useState(a() && ${expression})`,
96-
errors: [
97-
{
98-
type: T.LogicalExpression,
99-
messageId: "preferUseStateLazyInitialization",
100-
},
101-
],
102-
},
103-
{
104-
code: `import { useState } from "react"; useState(+${expression})`,
105-
errors: [
10679
{
107-
type: T.UnaryExpression,
80+
type: T.NewExpression,
10881
messageId: "preferUseStateLazyInitialization",
10982
},
11083
],
11184
},
11285
{
113-
code: `import { useState } from "react"; useState(-${expression})`,
86+
code: `import { useState } from "react"; useState(+getValue())`,
11487
errors: [
11588
{
116-
type: T.UnaryExpression,
89+
type: T.CallExpression,
11790
messageId: "preferUseStateLazyInitialization",
11891
},
11992
],
12093
},
12194
{
122-
code: `import { useState } from "react"; useState(~${expression})`,
95+
code: `import { useState } from "react"; useState(getValue() + 1)`,
12396
errors: [
12497
{
125-
type: T.UnaryExpression,
98+
type: T.CallExpression,
12699
messageId: "preferUseStateLazyInitialization",
127100
},
128101
],
129102
},
130103
{
131-
code: `import { useState } from "react"; useState(!${expression})`,
104+
code: `import { useState } from "react"; useState([getValue()])`,
132105
errors: [
133106
{
134-
type: T.UnaryExpression,
107+
type: T.CallExpression,
135108
messageId: "preferUseStateLazyInitialization",
136109
},
137110
],
138111
},
139112
{
140-
code: `import { useState } from "react"; useState(${expression} + 1)`,
113+
code: `import { useState } from "react"; useState({ a: getValue() })`,
141114
errors: [
142115
{
143-
type: T.BinaryExpression,
116+
type: T.CallExpression,
144117
messageId: "preferUseStateLazyInitialization",
145118
},
146119
],
147120
},
148121
{
149-
code: `import { useState } from "react"; useState(${expression} - 1)`,
150-
errors: [
151-
{
152-
type: T.BinaryExpression,
153-
messageId: "preferUseStateLazyInitialization",
154-
},
155-
],
156-
},
157-
{
158-
code: `import { useState } from "react"; useState([${expression}])`,
122+
code: tsx`
123+
import { useState, use } from 'react';
124+
125+
function Component({data}) {
126+
const [data, setData] = useState(data ? use(data) : getValue());
127+
return null;
128+
}
129+
`,
159130
errors: [
160131
{
161-
type: T.ArrayExpression,
132+
type: T.CallExpression,
162133
messageId: "preferUseStateLazyInitialization",
163134
},
164135
],
165-
},
166-
{
167-
code: `import { useState } from "react"; useState({ a: ${expression} })`,
168-
errors: [
169-
{
170-
type: T.ObjectExpression,
171-
messageId: "preferUseStateLazyInitialization",
136+
settings: {
137+
"react-x": {
138+
version: "19.0.0",
172139
},
173-
],
140+
},
174141
},
175142
{
176-
code: tsx`useLocalStorageState(1 || ${expression})`,
143+
code: tsx`useLocalStorageState(1 || getValue())`,
177144
errors: [
178145
{
179-
type: T.LogicalExpression,
146+
type: T.CallExpression,
180147
messageId: "preferUseStateLazyInitialization",
181148
},
182149
],
@@ -188,7 +155,7 @@ ruleTester.run(RULE_NAME, rule, {
188155
},
189156
},
190157
},
191-
]),
158+
],
192159
valid: [
193160
...allValid,
194161
"useState()",
@@ -260,7 +227,11 @@ ruleTester.run(RULE_NAME, rule, {
260227
'const { useState } = require("react"); useState(1 < 2 ? 3 : 4)',
261228
'const { useState } = require("react"); useState(1 == 2 ? 3 : 4)',
262229
'const { useState } = require("react"); useState(1 === 2 ? 3 : 4)',
230+
"const [id, setId] = useState(useId());",
263231
"const [state, setState] = useState(use(promise));",
232+
"const [serverData, setLikes] = useState(use(getLikes()));",
233+
"const [data, setData] = useState(use(getData()) || []);",
234+
"const [character, setCharacter] = useState(use(props.character) ?? undefined);",
264235
{
265236
code: tsx`
266237
import { useState, use } from 'react';
@@ -278,55 +249,11 @@ ruleTester.run(RULE_NAME, rule, {
278249
},
279250
},
280251
{
281-
code: tsx`
282-
import { useState, use } from 'react';
283-
284-
const promise = Promise.resolve();
285-
286-
function App() {
287-
const [state, setState] = useState(use(promise));
288-
289-
return null;
290-
}
291-
292-
export default App;
293-
`,
294-
settings: {
295-
"react-x": {
296-
version: "19.0.0",
297-
},
298-
},
299-
},
300-
{
301-
code: "useLocalStorageState()",
252+
code: "useLocalStorage(() => JSON.parse('{}'))",
302253
settings: {
303254
"react-x": {
304255
additionalHooks: {
305-
useState: ["useLocalStorageState"],
306-
},
307-
},
308-
},
309-
},
310-
{
311-
code: tsx`
312-
import { useState } from 'react';
313-
314-
function getValue() {
315-
return 0;
316-
}
317-
318-
function App() {
319-
const [count, setCount] = useState(() => getValue());
320-
321-
return null;
322-
}
323-
324-
export default App;
325-
`,
326-
settings: {
327-
"react-x": {
328-
additionalHooks: {
329-
useState: ["useLocalStorageState"],
256+
useState: ["useLocalStorage"],
330257
},
331258
},
332259
},

0 commit comments

Comments
 (0)