Skip to content

Commit

Permalink
[New] Symmetric useState hook variable names
Browse files Browse the repository at this point in the history
Ensure two symmetrically-named variables are destructured from useState hook calls
  • Loading branch information
duncanbeevers authored and ljharb committed Dec 9, 2020
1 parent 9be55ed commit c5b65a9
Show file tree
Hide file tree
Showing 5 changed files with 748 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -132,6 +132,7 @@ Enable the rules that you would like to use.
| | | [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Forbid using another component's propTypes |
| | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes |
| | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined |
| | | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables |
| | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState |
| | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. |
| | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys |
Expand Down
46 changes: 46 additions & 0 deletions docs/rules/hook-use-state.md
@@ -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();
}
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -16,6 +16,7 @@ const allRules = {
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'function-component-definition': require('./lib/rules/function-component-definition'),
'hook-use-state': require('./lib/rules/hook-use-state'),
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
Expand Down
152 changes: 152 additions & 0 deletions lib/rules/hook-use-state.js
@@ -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,
}
);
}
},
})),
};

0 comments on commit c5b65a9

Please sign in to comment.