Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New] add
hook-use-state
rule to enforce symmetric useState hook va…
…riable names Ensure two symmetrically-named variables are destructured from useState hook calls
- Loading branch information
1 parent
9be55ed
commit 4f54108
Showing
6 changed files
with
756 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Ensure destructuring and symmetric naming of useState hook value and setter variables (react/hook-use-state) | ||
|
||
## Rule Details | ||
|
||
This rule checks whether the value and setter variables destructured from a `React.useState()` call are named symmetrically. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
import React from 'react'; | ||
export default function useColor() { | ||
// useState call is not destructured into value + setter pair | ||
const useStateResult = React.useState(); | ||
return useStateResult; | ||
} | ||
``` | ||
|
||
```js | ||
import React from 'react'; | ||
export default function useColor() { | ||
// useState call is destructured into value + setter pair, but identifier | ||
// names do not follow the [thing, setThing] naming convention | ||
const [color, updateColor] = React.useState(); | ||
return useStateResult; | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
import React from 'react'; | ||
export default function useColor() { | ||
// useState call is destructured into value + setter pair whose identifiers | ||
// follow the [thing, setThing] naming convention | ||
const [color, setColor] = React.useState(); | ||
return [color, setColor]; | ||
} | ||
``` | ||
|
||
```js | ||
import React from 'react'; | ||
export default function useColor() { | ||
// useState result is directly returned | ||
return React.useState(); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/** | ||
* @fileoverview Ensure symmetric naming of useState hook value and setter variables | ||
* @author Duncan Beevers | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const Components = require('../util/Components'); | ||
const docsUrl = require('../util/docsUrl'); | ||
const report = require('../util/report'); | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
const messages = { | ||
useStateErrorMessage: 'useState call is not destructured into value + setter pair', | ||
}; | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'Ensure symmetric naming of useState hook value and setter variables', | ||
category: 'Best Practices', | ||
recommended: false, | ||
url: docsUrl('hook-use-state'), | ||
}, | ||
messages, | ||
schema: [], | ||
type: 'suggestion', | ||
hasSuggestions: true, | ||
}, | ||
|
||
create: Components.detect((context, components, util) => ({ | ||
CallExpression(node) { | ||
const isImmediateReturn = node.parent | ||
&& node.parent.type === 'ReturnStatement'; | ||
|
||
if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) { | ||
return; | ||
} | ||
|
||
const isDestructuringDeclarator = node.parent | ||
&& node.parent.type === 'VariableDeclarator' | ||
&& node.parent.id.type === 'ArrayPattern'; | ||
|
||
if (!isDestructuringDeclarator) { | ||
report( | ||
context, | ||
messages.useStateErrorMessage, | ||
'useStateErrorMessage', | ||
{ node } | ||
); | ||
return; | ||
} | ||
|
||
const variableNodes = node.parent.id.elements; | ||
const valueVariable = variableNodes[0]; | ||
const setterVariable = variableNodes[1]; | ||
|
||
const valueVariableName = valueVariable | ||
? valueVariable.name | ||
: undefined; | ||
|
||
const setterVariableName = setterVariable | ||
? setterVariable.name | ||
: undefined; | ||
|
||
const expectedSetterVariableName = valueVariableName ? ( | ||
`set${valueVariableName.charAt(0).toUpperCase()}${valueVariableName.slice(1)}` | ||
) : undefined; | ||
|
||
const isSymmetricGetterSetterPair = valueVariable | ||
&& setterVariable | ||
&& setterVariableName === expectedSetterVariableName | ||
&& variableNodes.length === 2; | ||
|
||
if (!isSymmetricGetterSetterPair) { | ||
const suggestions = [ | ||
{ | ||
desc: 'Destructure useState call into value + setter pair', | ||
fix: (fixer) => { | ||
const fix = fixer.replaceTextRange( | ||
node.parent.id.range, | ||
`[${valueVariableName}, ${expectedSetterVariableName}]` | ||
); | ||
|
||
return fix; | ||
}, | ||
}, | ||
]; | ||
|
||
const defaultReactImports = components.getDefaultReactImports(); | ||
const defaultReactImportSpecifier = defaultReactImports | ||
? defaultReactImports[0] | ||
: undefined; | ||
|
||
const defaultReactImportName = defaultReactImportSpecifier | ||
? defaultReactImportSpecifier.local.name | ||
: undefined; | ||
|
||
const namedReactImports = components.getNamedReactImports(); | ||
const useStateReactImportSpecifier = namedReactImports | ||
? namedReactImports.find((specifier) => specifier.imported.name === 'useState') | ||
: undefined; | ||
|
||
const isSingleGetter = valueVariable && variableNodes.length === 1; | ||
const isUseStateCalledWithSingleArgument = node.arguments.length === 1; | ||
if (isSingleGetter && isUseStateCalledWithSingleArgument) { | ||
const useMemoReactImportSpecifier = namedReactImports | ||
&& namedReactImports.find((specifier) => specifier.imported.name === 'useMemo'); | ||
|
||
let useMemoCode; | ||
if (useMemoReactImportSpecifier) { | ||
useMemoCode = useMemoReactImportSpecifier.local.name; | ||
} else if (defaultReactImportName) { | ||
useMemoCode = `${defaultReactImportName}.useMemo`; | ||
} else { | ||
useMemoCode = 'useMemo'; | ||
} | ||
|
||
suggestions.unshift({ | ||
desc: 'Replace useState call with useMemo', | ||
fix: (fixer) => [ | ||
// Add useMemo import, if necessary | ||
useStateReactImportSpecifier | ||
&& (!useMemoReactImportSpecifier || defaultReactImportName) | ||
&& fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'), | ||
// Convert single-value destructure to simple assignment | ||
fixer.replaceTextRange(node.parent.id.range, valueVariableName), | ||
// Convert useState call to useMemo + arrow function + dependency array | ||
fixer.replaceTextRange( | ||
node.range, | ||
`${useMemoCode}(() => ${context.getSourceCode().getText(node.arguments[0])}, [])` | ||
), | ||
].filter(Boolean), | ||
}); | ||
} | ||
|
||
report( | ||
context, | ||
messages.useStateErrorMessage, | ||
'useStateErrorMessage', | ||
{ | ||
node: node.parent.id, | ||
suggest: suggestions, | ||
} | ||
); | ||
} | ||
}, | ||
})), | ||
}; |
Oops, something went wrong.