Skip to content

Commit 01e6a4d

Browse files
committed
[New] Symmetric useState hook variable names
Ensure two symmetrically-named variables are destructured from useState hook calls
1 parent 05d35ad commit 01e6a4d

File tree

5 files changed

+298
-0
lines changed

5 files changed

+298
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ Enable the rules that you would like to use.
115115
| | | [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Forbid using another component's propTypes |
116116
| | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes |
117117
| | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined |
118+
| | 🔧 | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables |
118119
| | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState |
119120
| | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. |
120121
| | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys |

docs/rules/hook-use-state.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Ensure destructuring and symmetric naming of useState hook value and setter variables (react/hook-use-state)
2+
3+
**Fixable:** In some cases, this rule is automatically fixable using the `--fix` flag on the command line.
4+
5+
## Rule Details
6+
7+
This rule checks whether the value and setter variables destructured from a `React.useState()` call are named symmetrically.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```js
12+
const useStateResult = React.useState();
13+
```
14+
15+
```js
16+
const [color, updateColor] = React.useState();
17+
```
18+
19+
Examples of **correct** code for this rule:
20+
21+
```js
22+
const [color, setColor] = React.useState();
23+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const allRules = {
1616
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
1717
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
1818
'function-component-definition': require('./lib/rules/function-component-definition'),
19+
'hook-use-state': require('./lib/rules/hook-use-state'),
1920
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
2021
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
2122
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),

lib/rules/hook-use-state.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @fileoverview Ensure symmetric naming of useState hook value and setter variables
3+
* @author Duncan Beevers
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
const USE_STATE_ERROR_MESSAGE = 'useStateErrorMessage';
15+
16+
module.exports = {
17+
meta: {
18+
docs: {
19+
description: 'Ensure symmetric naming of useState hook value and setter variables',
20+
category: 'Best Practices',
21+
recommended: false,
22+
url: docsUrl('hook-use-state')
23+
},
24+
fixable: 'code',
25+
messages: {
26+
[USE_STATE_ERROR_MESSAGE]: 'setState call is not destructured into value + setter pair'
27+
},
28+
schema: [{
29+
type: 'object',
30+
additionalProperties: false
31+
}]
32+
},
33+
34+
create(context) {
35+
return {
36+
CallExpression(node) {
37+
const isReactUseStateCall = (
38+
node.callee.type === 'MemberExpression'
39+
&& node.callee.object.type === 'Identifier'
40+
&& node.callee.object.name === 'React'
41+
&& node.callee.property.type === 'Identifier'
42+
&& node.callee.property.name === 'useState'
43+
);
44+
45+
const isUseStateCall = (
46+
node.callee.type === 'Identifier'
47+
&& node.callee.name === 'useState'
48+
);
49+
50+
// Ignore unless this is a useState() or React.useState() call.
51+
if (!isReactUseStateCall && !isUseStateCall) {
52+
return;
53+
}
54+
55+
const isDestructuringDeclarator = (
56+
node.parent.type === 'VariableDeclarator'
57+
&& node.parent.id.type === 'ArrayPattern'
58+
);
59+
60+
if (!isDestructuringDeclarator) {
61+
context.report({node, messageId: USE_STATE_ERROR_MESSAGE});
62+
return;
63+
}
64+
65+
const variableNodes = node.parent.id.elements;
66+
const valueVariable = variableNodes[0];
67+
const setterVariable = variableNodes[1];
68+
69+
const valueVariableName = valueVariable
70+
? valueVariable.name
71+
: undefined;
72+
73+
const setterVariableName = setterVariable
74+
? setterVariable.name
75+
: undefined;
76+
77+
const expectedSetterVariableName = valueVariableName ? (
78+
`set${
79+
valueVariableName.charAt(0).toUpperCase()
80+
}${valueVariableName.slice(1)}`
81+
) : undefined;
82+
83+
if (
84+
!valueVariable
85+
|| !setterVariable
86+
|| setterVariableName !== expectedSetterVariableName
87+
|| variableNodes.length !== 2
88+
) {
89+
context.report({
90+
node: node.parent.id,
91+
messageId: USE_STATE_ERROR_MESSAGE,
92+
fix: valueVariableName ? (fixer) => fixer.replaceTextRange(
93+
[node.parent.id.range[0], node.parent.id.range[1]],
94+
`[${valueVariableName}, ${expectedSetterVariableName}]`
95+
) : undefined
96+
});
97+
}
98+
}
99+
};
100+
}
101+
};

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

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* @fileoverview Ensure symmetric naming of setState hook value and setter variables
3+
* @author Duncan Beevers
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const RuleTester = require('eslint').RuleTester;
13+
const rule = require('../../../lib/rules/hook-use-state');
14+
const parsers = require('../../helpers/parsers');
15+
16+
// ------------------------------------------------------------------------------
17+
// Tests
18+
// ------------------------------------------------------------------------------
19+
20+
const ruleTester = new RuleTester({
21+
parserOptions: {
22+
ecmaVersion: 2018,
23+
sourceType: 'module'
24+
}
25+
});
26+
27+
const tests = {
28+
valid: [
29+
{
30+
code: 'const [color, setColor] = useState()'
31+
},
32+
{
33+
code: 'const [color, setColor] = useState(\'#ffffff\')'
34+
},
35+
{
36+
code: 'const [color, setColor] = React.useState()'
37+
},
38+
{
39+
code: 'const [color1, setColor1] = useState()'
40+
},
41+
{
42+
code: 'const [color, setColor] = useState<string>()',
43+
parser: parsers.TYPESCRIPT_ESLINT
44+
},
45+
{
46+
code: 'const [color, setColor] = useState<string>(\'#ffffff\')',
47+
parser: parsers.TYPESCRIPT_ESLINT
48+
}
49+
].concat(parsers.TS([
50+
{
51+
code: 'const [color, setColor] = useState<string>()',
52+
parser: parsers['@TYPESCRIPT_ESLINT']
53+
},
54+
{
55+
code: 'const [color, setColor] = useState<string>(\'#ffffff\')',
56+
parser: parsers['@TYPESCRIPT_ESLINT']
57+
}
58+
])
59+
),
60+
invalid: [
61+
{
62+
code: 'useState()',
63+
errors: [{
64+
message: 'setState call is not destructured into value + setter pair'
65+
}]
66+
},
67+
{
68+
code: 'const result = useState()',
69+
errors: [{
70+
message: 'setState call is not destructured into value + setter pair'
71+
}]
72+
},
73+
{
74+
code: 'const result = React.useState()',
75+
errors: [{
76+
message: 'setState call is not destructured into value + setter pair'
77+
}]
78+
},
79+
{
80+
code: 'const [, , extra1] = useState()',
81+
errors: [{
82+
message: 'setState call is not destructured into value + setter pair'
83+
}]
84+
},
85+
{
86+
code: 'const [, setColor] = useState()',
87+
errors: [{
88+
message: 'setState call is not destructured into value + setter pair'
89+
}]
90+
},
91+
{
92+
code: 'const { color } = useState()',
93+
errors: [{
94+
message: 'setState call is not destructured into value + setter pair'
95+
}]
96+
},
97+
{
98+
code: 'const [] = useState()',
99+
errors: [{
100+
message: 'setState call is not destructured into value + setter pair'
101+
}]
102+
},
103+
{
104+
code: 'const [, , , ,] = useState()',
105+
errors: [{
106+
message: 'setState call is not destructured into value + setter pair'
107+
}]
108+
},
109+
{
110+
code: 'const [color] = useState()',
111+
errors: [{
112+
message: 'setState call is not destructured into value + setter pair'
113+
}],
114+
output: 'const [color, setColor] = useState()'
115+
},
116+
{
117+
code: 'const [color, , extra1] = useState()',
118+
errors: [{
119+
message: 'setState call is not destructured into value + setter pair'
120+
}],
121+
output: 'const [color, setColor] = useState()'
122+
},
123+
{
124+
code: 'const [color, setColor, extra1, extra2, extra3] = useState()',
125+
errors: [{
126+
message: 'setState call is not destructured into value + setter pair'
127+
}],
128+
output: 'const [color, setColor] = useState()'
129+
},
130+
{
131+
code: 'const [, makeColor] = useState()',
132+
errors: [{
133+
message: 'setState call is not destructured into value + setter pair'
134+
}]
135+
},
136+
{
137+
code: 'const [color, setFlavor, extraneous] = useState()',
138+
errors: [{
139+
message: 'setState call is not destructured into value + setter pair'
140+
}],
141+
output: 'const [color, setColor] = useState()'
142+
},
143+
{
144+
code: 'const [color, setFlavor] = useState()',
145+
errors: [{
146+
message: 'setState call is not destructured into value + setter pair'
147+
}],
148+
output: 'const [color, setColor] = useState()'
149+
},
150+
{
151+
code: 'const [color, setFlavor] = useState<string>()',
152+
errors: [{
153+
message: 'setState call is not destructured into value + setter pair'
154+
}],
155+
output: 'const [color, setColor] = useState<string>()',
156+
parser: parsers.TYPESCRIPT_ESLINT
157+
}
158+
].concat(
159+
parsers.TS([
160+
{
161+
code: 'const [color, setFlavor] = useState<string>()',
162+
errors: [{
163+
message: 'setState call is not destructured into value + setter pair'
164+
}],
165+
output: 'const [color, setColor] = useState<string>()',
166+
parser: parsers['@TYPESCRIPT_ESLINT']
167+
}
168+
])
169+
)
170+
};
171+
172+
ruleTester.run('hook-set-state-names', rule, tests);

0 commit comments

Comments
 (0)