Skip to content

Commit f66510b

Browse files
committed
fixup! [New] Symmetric useState hook variable names
1 parent 9fae46a commit f66510b

File tree

2 files changed

+146
-12
lines changed

2 files changed

+146
-12
lines changed

lib/rules/hook-use-state.js

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ module.exports = {
2828
fixable: 'code',
2929
messages,
3030
schema: [],
31+
type: 'suggestion',
32+
hasSuggestions: true,
3133
},
3234

3335
create: Components.detect((context, components) => ({
@@ -134,18 +136,75 @@ module.exports = {
134136
&& variableNodes.length === 2;
135137

136138
if (!isSymmetricGetterSetterPair) {
137-
report(
138-
context,
139-
messages.useStateErrorMessage,
140-
'useStateErrorMessage',
141-
{
142-
node: node.parent.id,
143-
fix: valueVariableName ? (fixer) => fixer.replaceTextRange(
144-
[node.parent.id.range[0], node.parent.id.range[1]],
145-
`[${valueVariableName}, ${expectedSetterVariableName}]`
146-
) : undefined,
147-
}
148-
);
139+
const isSingleGetter = valueVariable && variableNodes.length === 1;
140+
const isUseStateCalledWithSingleArgument = node.arguments.length === 1;
141+
if (isSingleGetter && isUseStateCalledWithSingleArgument) {
142+
const useMemoReactImportSpecifier = namedReactImports ? namedReactImports.find((specifier) => specifier.imported.name === 'useMemo') : undefined;
143+
const sourceCode = context.getSourceCode();
144+
const useStateArgumentSourceCode = sourceCode.getText(node.arguments[0]);
145+
146+
report(
147+
context,
148+
messages.useStateErrorMessage,
149+
'useStateErrorMessage',
150+
{
151+
node: node.parent.id,
152+
suggest: [
153+
{
154+
desc: 'Replace useState call with useMemo',
155+
fix: (fixer) => {
156+
const useMemoImportName = useMemoReactImportSpecifier && useMemoReactImportSpecifier.local.name;
157+
158+
const useMemoReference = useMemoImportName
159+
|| (defaultReactImportName
160+
&& `${defaultReactImportName}.useMemo`)
161+
|| 'useMemo';
162+
163+
const fixes = [
164+
// Add useMemo import, if necessary
165+
useStateReactImportSpecifier
166+
&& (!useMemoReactImportSpecifier || defaultReactImportName)
167+
&& fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'),
168+
// Convert single-value destructure to simple assignment
169+
fixer.replaceTextRange(node.parent.id.range, valueVariableName),
170+
// Convert useState call to useMemo + arrow function + dependency array
171+
fixer.replaceTextRange(
172+
node.range,
173+
`${useMemoReference}(() => ${useStateArgumentSourceCode}, [])`
174+
),
175+
].filter(Boolean);
176+
177+
return fixes;
178+
},
179+
},
180+
{
181+
desc: 'Destructure useState call into value + setter pair',
182+
fix: (fixer) => {
183+
const fix = fixer.replaceTextRange(
184+
node.parent.id.range,
185+
`[${valueVariableName}, ${expectedSetterVariableName}]`
186+
);
187+
188+
return fix;
189+
},
190+
},
191+
].filter(Boolean),
192+
}
193+
);
194+
} else {
195+
report(
196+
context,
197+
messages.useStateErrorMessage,
198+
'useStateErrorMessage',
199+
{
200+
node: node.parent.id,
201+
fix: valueVariableName ? (fixer) => fixer.replaceTextRange(
202+
[node.parent.id.range[0], node.parent.id.range[1]],
203+
`[${valueVariableName}, ${expectedSetterVariableName}]`
204+
) : undefined,
205+
}
206+
);
207+
}
149208
}
150209
},
151210
})),

tests/lib/rules/hook-use-state.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,81 @@ const tests = {
236236
const [color, setColor] = useState()
237237
}`,
238238
},
239+
{
240+
code: `import { useState } from 'react'
241+
export default function useColor(initialColor) {
242+
const [color] = useState(initialColor)
243+
}`,
244+
errors: [{
245+
message: 'useState call is not destructured into value + setter pair',
246+
suggestions: [
247+
{
248+
desc: 'Replace useState call with useMemo',
249+
output: `import { useState, useMemo } from 'react'
250+
export default function useColor(initialColor) {
251+
const color = useMemo(() => initialColor, [])
252+
}`,
253+
},
254+
{
255+
desc: 'Destructure useState call into value + setter pair',
256+
output: `import { useState } from 'react'
257+
export default function useColor(initialColor) {
258+
const [color, setColor] = useState(initialColor)
259+
}`,
260+
},
261+
],
262+
}],
263+
},
264+
{
265+
code: `import { useState, useMemo as useMemoAlternative } from 'react'
266+
export default function useColor(initialColor) {
267+
const [color] = useState(initialColor)
268+
}`,
269+
errors: [{
270+
message: 'useState call is not destructured into value + setter pair',
271+
suggestions: [
272+
{
273+
desc: 'Replace useState call with useMemo',
274+
output: `import { useState, useMemo as useMemoAlternative } from 'react'
275+
export default function useColor(initialColor) {
276+
const color = useMemoAlternative(() => initialColor, [])
277+
}`,
278+
},
279+
{
280+
desc: 'Destructure useState call into value + setter pair',
281+
output: `import { useState, useMemo as useMemoAlternative } from 'react'
282+
export default function useColor(initialColor) {
283+
const [color, setColor] = useState(initialColor)
284+
}`,
285+
},
286+
],
287+
}],
288+
},
289+
{
290+
code: `import React from 'react'
291+
export default function useColor(initialColor) {
292+
const [color] = React.useState(initialColor)
293+
}`,
294+
errors: [{
295+
message: 'useState call is not destructured into value + setter pair',
296+
suggestions: [
297+
{
298+
desc: 'Replace useState call with useMemo',
299+
output: `import React from 'react'
300+
export default function useColor(initialColor) {
301+
const color = React.useMemo(() => initialColor, [])
302+
}`,
303+
},
304+
{
305+
desc: 'Destructure useState call into value + setter pair',
306+
output: `import React from 'react'
307+
export default function useColor(initialColor) {
308+
const [color, setColor] = React.useState(initialColor)
309+
}`,
310+
},
311+
],
312+
}],
313+
},
239314
{
240315
code: `import { useState } from 'react'
241316
export default function useColor() {

0 commit comments

Comments
 (0)