Skip to content

Commit 2d0d49b

Browse files
authored
feat: useSelector-prefer-selectors (DianaSuvorova#54)
* feat: useSelector-prefer-selectors Add rule that enforces the use of selector function on redux `useSelector` hook. * docs: add docs to useSelector-prefer-selectors
1 parent d3aa304 commit 2d0d49b

File tree

5 files changed

+176
-0
lines changed

5 files changed

+176
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ To configure individual rules:
5555
* [react-redux/mapStateToProps-prefer-hoisted](docs/rules/mapStateToProps-prefer-hoisted.md) Flags generation of copies of same-by-value but different-by-reference props.
5656
* [react-redux/mapStateToProps-prefer-parameters-names](docs/rules/mapStateToProps-prefer-parameters-names.md) Enforces that all mapStateToProps parameters have specific names.
5757
* [react-redux/mapStateToProps-prefer-selectors](docs/rules/mapStateToProps-prefer-selectors.md) Enforces that all mapStateToProps properties use selector functions.
58+
* [react-redux/useSelector-prefer-selectors](docs/rules/useSelector-prefer-selectors.md) Enforces that all useSelector properties use selector functions.
5859
* [react-redux/no-unused-prop-types](docs/rules/no-unused-prop-types.md) Extension of a react's no-unused-prop-types rule filtering out false positive used in redux context.
5960
* [react-redux/prefer-separate-component-file](docs/rules/prefer-separate-component-file.md) Enforces that all connected components are defined in a separate file.
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Enforces that all useSelector hooks use named selector functions. (react-redux/useSelector-prefer-selectors)
2+
3+
Using selectors in `useSelector` to pull data from the store or [compute derived data](https://redux.js.org/recipes/computing-derived-data#composing-selectors) allows you to decouple your containers from the state architecture and more easily enable memoization. This rule will ensure that every hook utilizes a named selector.
4+
5+
## Rule details
6+
7+
The following pattern is considered incorrect:
8+
9+
```js
10+
const property = useSelector((state) => state.property)
11+
const property = useSelector(function (state) { return state.property })
12+
```
13+
14+
The following patterns are considered correct:
15+
16+
```js
17+
const selector = (state) => state.property
18+
19+
function Component() {
20+
const property = useSelector(selector)
21+
// ...
22+
}
23+
```
24+
25+
## Rule Options
26+
27+
```js
28+
...
29+
"react-redux/useSelector-prefer-selectors": [<enabled>, {
30+
"matching": <string>
31+
"validateParams": <boolean>
32+
}]
33+
...
34+
```
35+
36+
### `matching`
37+
If provided, validates the name of the selector functions against the RegExp pattern provided.
38+
39+
```js
40+
// .eslintrc
41+
{
42+
"react-redux/useSelector-prefer-selectors": ["error", { matching: "^.*Selector$"}]
43+
}
44+
45+
// container.js
46+
const propertyA = useSelector(aSelector) // success
47+
const propertyB = useSelector(selectB) // failure
48+
```
49+
50+
```js
51+
// .eslintrc
52+
{
53+
"react-redux/mapStateToProps-prefer-selectors": ["error", { matching: "^get.*FromState$"}]
54+
}
55+
56+
// container.js
57+
const propertyA = useSelector(getAFromState) // success
58+
const propertyB = useSelector(getB) // failure
59+
```

index.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const rules = {
88
'mapStateToProps-prefer-hoisted': require('./lib/rules/mapStateToProps-prefer-hoisted'),
99
'mapStateToProps-prefer-parameters-names': require('./lib/rules/mapStateToProps-prefer-parameters-names'),
1010
'mapStateToProps-prefer-selectors': require('./lib/rules/mapStateToProps-prefer-selectors'),
11+
'useSelector-prefer-selectors': require('./lib/rules/useSelector-prefer-selectors'),
1112
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
1213
'prefer-separate-component-file': require('./lib/rules/prefer-separate-component-file'),
1314
};
@@ -36,6 +37,7 @@ module.exports = {
3637
'react-redux/mapStateToProps-no-store': 2,
3738
'react-redux/mapStateToProps-prefer-hoisted': 2,
3839
'react-redux/mapStateToProps-prefer-parameters-names': 2,
40+
'react-redux/useSelector-prefer-selectors': 2,
3941
'react-redux/no-unused-prop-types': 2,
4042
'react-redux/prefer-separate-component-file': 1,
4143
},
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function isUseSelector(node) {
2+
return node.callee.name === 'useSelector';
3+
}
4+
5+
function reportWrongName(context, node, functionName, matching) {
6+
context.report({
7+
message: `useSelector selector "${functionName}" does not match "${matching}".`,
8+
node,
9+
});
10+
}
11+
12+
function reportNoSelector(context, node) {
13+
context.report({
14+
message: 'useSelector should use a named selector function.',
15+
node,
16+
});
17+
}
18+
19+
module.exports = function (context) {
20+
const config = context.options[0] || {};
21+
return {
22+
CallExpression(node) {
23+
if (!isUseSelector(node)) return;
24+
const selector = node.arguments && node.arguments[0];
25+
if (selector && (
26+
selector.type === 'ArrowFunctionExpression' ||
27+
selector.type === 'FunctionExpression')
28+
) {
29+
reportNoSelector(context, node);
30+
} else if (
31+
selector.type === 'Identifier' &&
32+
config.matching &&
33+
!selector.name.match(new RegExp(config.matching))
34+
) {
35+
reportWrongName(context, node, selector.name, config.matching);
36+
}
37+
},
38+
};
39+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require('babel-eslint');
2+
3+
const rule = require('../../../lib/rules/useSelector-prefer-selectors');
4+
const RuleTester = require('eslint').RuleTester;
5+
6+
const parserOptions = {
7+
ecmaVersion: 6,
8+
sourceType: 'module',
9+
ecmaFeatures: {
10+
experimentalObjectRestSpread: true,
11+
},
12+
};
13+
14+
const ruleTester = new RuleTester({ parserOptions });
15+
16+
ruleTester.run('useSelector-prefer-selectors', rule, {
17+
valid: [
18+
'const property = useSelector(xSelector)',
19+
{
20+
code: 'const property = useSelector(xSelector)',
21+
options: [{
22+
matching: '^.*Selector$',
23+
}],
24+
},
25+
{
26+
code: 'const property = useSelector(getX)',
27+
options: [{
28+
matching: '^get.*$',
29+
}],
30+
},
31+
{
32+
code: 'const property = useSelector(selector)',
33+
options: [{
34+
matching: '^selector$',
35+
}],
36+
},
37+
],
38+
invalid: [{
39+
code: 'const property = useSelector((state) => state.x)',
40+
errors: [
41+
{
42+
message: 'useSelector should use a named selector function.',
43+
},
44+
],
45+
}, {
46+
code: 'const property = useSelector(function(state) { return state.x })',
47+
errors: [{
48+
message: 'useSelector should use a named selector function.',
49+
}],
50+
}, {
51+
code: 'const property = useSelector(xSelector)',
52+
options: [{
53+
matching: '^get.*$',
54+
}],
55+
errors: [{
56+
message: 'useSelector selector "xSelector" does not match "^get.*$".',
57+
}],
58+
}, {
59+
code: 'const property = useSelector(getX)',
60+
options: [{
61+
matching: '^.*Selector$',
62+
}],
63+
errors: [{
64+
message: 'useSelector selector "getX" does not match "^.*Selector$".',
65+
}],
66+
}, {
67+
code: 'const property = useSelector(selectorr)',
68+
options: [{
69+
matching: '^selector$',
70+
}],
71+
errors: [{
72+
message: 'useSelector selector "selectorr" does not match "^selector$".',
73+
}],
74+
}],
75+
});

0 commit comments

Comments
 (0)