Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New Rule] prefer-use-state-lazy-initialization #3579

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -378,6 +378,7 @@ module.exports = [
| [prefer-exact-props](docs/rules/prefer-exact-props.md) | Prefer exact proptype definitions | | | | | |
| [prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Enforce that props are read-only | | | 🔧 | | |
| [prefer-stateless-function](docs/rules/prefer-stateless-function.md) | Enforce stateless components to be written as a pure function | | | | | |
| [prefer-use-state-lazy-initialization](docs/rules/prefer-use-state-lazy-initialization.md) | Disallow function calls in useState that aren't wrapped in an initializer function | | | | | |
| [prop-types](docs/rules/prop-types.md) | Disallow missing props validation in a React component definition | ☑️ | | | | |
| [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md) | Disallow missing React when using JSX | ☑️ | 🏃 | | | |
| [require-default-props](docs/rules/require-default-props.md) | Enforce a defaultProps definition for every prop that is not a required prop | | | | | |
Expand Down
25 changes: 25 additions & 0 deletions docs/rules/prefer-use-state-lazy-initialization.md
@@ -0,0 +1,25 @@
# Disallow function calls in useState that aren't wrapped in an initializer function (`react/prefer-use-state-lazy-initialization`)

<!-- end auto-generated rule header -->

A function can be invoked inside a useState call to help create its initial state. However, subsequent renders will still invoke the function while discarding its return value. This is wasteful and can cause performance issues if the function call is expensive. To combat this issue React allows useState calls to use an [initializer function](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state) which will only be called on the first render.

## Rule Details

This rule will warn you about function calls made inside useState calls.

Examples of **incorrect** code for this rule:

```js
const [value, setValue] = useState(generateTodos());
```

Examples of **correct** code for this rule:

```js
const [value, setValue] = useState(() => generateTodos());
```

## Further Reading

- [Official React documentation on useState](https://react.dev/reference/react/useState)
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -90,6 +90,7 @@ module.exports = {
'prefer-exact-props': require('./prefer-exact-props'),
'prefer-read-only-props': require('./prefer-read-only-props'),
'prefer-stateless-function': require('./prefer-stateless-function'),
'prefer-use-state-lazy-initialization': require('./prefer-use-state-lazy-initialization'),
'prop-types': require('./prop-types'),
'react-in-jsx-scope': require('./react-in-jsx-scope'),
'require-default-props': require('./require-default-props'),
Expand Down
82 changes: 82 additions & 0 deletions lib/rules/prefer-use-state-lazy-initialization.js
@@ -0,0 +1,82 @@
/**
* @fileoverview Detects function calls in useState and suggests using lazy initialization instead.
* @author Patrick Gillespie
*/

'use strict';

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
"Disallow function calls in useState that aren't wrapped in an initializer function",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
messages: {
useLazyInitialization:
'To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: useState(() => getValue())',
},
},

// rule takes inspiration from https://github.com/facebook/react/issues/26520
create(context) {
// variables should be defined here
const ALLOW_LIST = Object.freeze(['Boolean', 'String']);

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------

// any helper functions should go here or else delete this section

const hasFunctionCall = (node) => {
if (
node.type === 'CallExpression'
&& ALLOW_LIST.indexOf(node.callee.name) === -1
) {
return true;
}
if (node.type === 'ConditionalExpression') {
return (
hasFunctionCall(node.test)
|| hasFunctionCall(node.consequent)
|| hasFunctionCall(node.alternate)
);
}
if (
node.type === 'LogicalExpression'
|| node.type === 'BinaryExpression'
) {
return hasFunctionCall(node.left) || hasFunctionCall(node.right);
}
return false;
};

//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------

return {
CallExpression(node) {
// @ts-ignore
if (node.callee && node.callee.name === 'useState') {
if (node.arguments.length > 0) {
const useStateInput = node.arguments[0];
if (hasFunctionCall(useStateInput)) {
context.report({ node, messageId: 'useLazyInitialization' });
}
}
}
},
};
},
};
134 changes: 134 additions & 0 deletions tests/lib/rules/prefer-use-state-lazy-initialization.js
@@ -0,0 +1,134 @@
/**
* @fileoverview Detects function calls in useState and suggests using lazy initialization instead.
* @author Patrick Gillespie
*/

'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/prefer-use-state-lazy-initialization');

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();
ruleTester.run('prefer-use-state-lazy-initialization', rule, {
valid: [
// give me some code that won't trigger a warning
'useState()',
'useState("")',
'useState(true)',
'useState(false)',
'useState(null)',
'useState(undefined)',
'useState(1)',
'useState("test")',
'useState(value)',
'useState(object.value)',
'useState(1 || 2)',
'useState(1 || 2 || 3 < 4)',
'useState(1 && 2)',
'useState(1 < 2)',
'useState(1 < 2 ? 3 : 4)',
'useState(1 == 2 ? 3 : 4)',
'useState(1 === 2 ? 3 : 4)',
],

invalid: [
{
code: 'useState(1 || getValue())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(2 < getValue())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(getValue())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(getValue(1, 2, 3))',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a ? b : c())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a() ? b : c)',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a ? (b ? b1() : b2) : c)',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a() && b)',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a && b())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
{
code: 'useState(a() && b())',
errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
],
});