Skip to content

Commit

Permalink
feat: useSelector-prefer-selectors (#54)
Browse files Browse the repository at this point in the history
* feat: useSelector-prefer-selectors

Add rule that enforces the use of selector function on redux
`useSelector` hook.

* docs: add docs to useSelector-prefer-selectors
  • Loading branch information
martini97 authored Aug 28, 2020
1 parent d3aa304 commit 2d0d49b
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@ To configure individual rules:
* [react-redux/mapStateToProps-prefer-hoisted](docs/rules/mapStateToProps-prefer-hoisted.md) Flags generation of copies of same-by-value but different-by-reference props.
* [react-redux/mapStateToProps-prefer-parameters-names](docs/rules/mapStateToProps-prefer-parameters-names.md) Enforces that all mapStateToProps parameters have specific names.
* [react-redux/mapStateToProps-prefer-selectors](docs/rules/mapStateToProps-prefer-selectors.md) Enforces that all mapStateToProps properties use selector functions.
* [react-redux/useSelector-prefer-selectors](docs/rules/useSelector-prefer-selectors.md) Enforces that all useSelector properties use selector functions.
* [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.
* [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 changes: 59 additions & 0 deletions docs/rules/useSelector-prefer-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Enforces that all useSelector hooks use named selector functions. (react-redux/useSelector-prefer-selectors)

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.

## Rule details

The following pattern is considered incorrect:

```js
const property = useSelector((state) => state.property)
const property = useSelector(function (state) { return state.property })
```

The following patterns are considered correct:

```js
const selector = (state) => state.property

function Component() {
const property = useSelector(selector)
// ...
}
```

## Rule Options

```js
...
"react-redux/useSelector-prefer-selectors": [<enabled>, {
"matching": <string>
"validateParams": <boolean>
}]
...
```

### `matching`
If provided, validates the name of the selector functions against the RegExp pattern provided.

```js
// .eslintrc
{
"react-redux/useSelector-prefer-selectors": ["error", { matching: "^.*Selector$"}]
}

// container.js
const propertyA = useSelector(aSelector) // success
const propertyB = useSelector(selectB) // failure
```

```js
// .eslintrc
{
"react-redux/mapStateToProps-prefer-selectors": ["error", { matching: "^get.*FromState$"}]
}

// container.js
const propertyA = useSelector(getAFromState) // success
const propertyB = useSelector(getB) // failure
```
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const rules = {
'mapStateToProps-prefer-hoisted': require('./lib/rules/mapStateToProps-prefer-hoisted'),
'mapStateToProps-prefer-parameters-names': require('./lib/rules/mapStateToProps-prefer-parameters-names'),
'mapStateToProps-prefer-selectors': require('./lib/rules/mapStateToProps-prefer-selectors'),
'useSelector-prefer-selectors': require('./lib/rules/useSelector-prefer-selectors'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'prefer-separate-component-file': require('./lib/rules/prefer-separate-component-file'),
};
Expand Down Expand Up @@ -36,6 +37,7 @@ module.exports = {
'react-redux/mapStateToProps-no-store': 2,
'react-redux/mapStateToProps-prefer-hoisted': 2,
'react-redux/mapStateToProps-prefer-parameters-names': 2,
'react-redux/useSelector-prefer-selectors': 2,
'react-redux/no-unused-prop-types': 2,
'react-redux/prefer-separate-component-file': 1,
},
Expand Down
39 changes: 39 additions & 0 deletions lib/rules/useSelector-prefer-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
function isUseSelector(node) {
return node.callee.name === 'useSelector';
}

function reportWrongName(context, node, functionName, matching) {
context.report({
message: `useSelector selector "${functionName}" does not match "${matching}".`,
node,
});
}

function reportNoSelector(context, node) {
context.report({
message: 'useSelector should use a named selector function.',
node,
});
}

module.exports = function (context) {
const config = context.options[0] || {};
return {
CallExpression(node) {
if (!isUseSelector(node)) return;
const selector = node.arguments && node.arguments[0];
if (selector && (
selector.type === 'ArrowFunctionExpression' ||
selector.type === 'FunctionExpression')
) {
reportNoSelector(context, node);
} else if (
selector.type === 'Identifier' &&
config.matching &&
!selector.name.match(new RegExp(config.matching))
) {
reportWrongName(context, node, selector.name, config.matching);
}
},
};
};
75 changes: 75 additions & 0 deletions tests/lib/rules/useSelector-prefer-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
require('babel-eslint');

const rule = require('../../../lib/rules/useSelector-prefer-selectors');
const RuleTester = require('eslint').RuleTester;

const parserOptions = {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
};

const ruleTester = new RuleTester({ parserOptions });

ruleTester.run('useSelector-prefer-selectors', rule, {
valid: [
'const property = useSelector(xSelector)',
{
code: 'const property = useSelector(xSelector)',
options: [{
matching: '^.*Selector$',
}],
},
{
code: 'const property = useSelector(getX)',
options: [{
matching: '^get.*$',
}],
},
{
code: 'const property = useSelector(selector)',
options: [{
matching: '^selector$',
}],
},
],
invalid: [{
code: 'const property = useSelector((state) => state.x)',
errors: [
{
message: 'useSelector should use a named selector function.',
},
],
}, {
code: 'const property = useSelector(function(state) { return state.x })',
errors: [{
message: 'useSelector should use a named selector function.',
}],
}, {
code: 'const property = useSelector(xSelector)',
options: [{
matching: '^get.*$',
}],
errors: [{
message: 'useSelector selector "xSelector" does not match "^get.*$".',
}],
}, {
code: 'const property = useSelector(getX)',
options: [{
matching: '^.*Selector$',
}],
errors: [{
message: 'useSelector selector "getX" does not match "^.*Selector$".',
}],
}, {
code: 'const property = useSelector(selectorr)',
options: [{
matching: '^selector$',
}],
errors: [{
message: 'useSelector selector "selectorr" does not match "^selector$".',
}],
}],
});

0 comments on commit 2d0d49b

Please sign in to comment.