Skip to content

Commit ebb739a

Browse files
committed
Initial work for useState lazy initialization rule
1 parent 13f5c19 commit ebb739a

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Disallow function calls in useState that aren't wrapped in an initializer function (`react/prefer-use-state-lazy-initialization`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
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.
6+
7+
## Rule Details
8+
9+
This rule will warn you about function calls made inside useState calls.
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```js
14+
const [value, setValue] = useState(generateTodos());
15+
```
16+
17+
Examples of **correct** code for this rule:
18+
19+
```js
20+
const [value, setValue] = useState(() => generateTodos());
21+
```
22+
23+
## Further Reading
24+
25+
- [Official React documentation on useState](https://react.dev/reference/react/useState)

lib/rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ module.exports = {
9090
'prefer-exact-props': require('./prefer-exact-props'),
9191
'prefer-read-only-props': require('./prefer-read-only-props'),
9292
'prefer-stateless-function': require('./prefer-stateless-function'),
93+
'prefer-use-state-lazy-initialization': require('./prefer-use-state-lazy-initialization'),
9394
'prop-types': require('./prop-types'),
9495
'react-in-jsx-scope': require('./react-in-jsx-scope'),
9596
'require-default-props': require('./require-default-props'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @fileoverview Detects function calls in useState and suggests using lazy initialization instead.
3+
* @author Patrick Gillespie
4+
*/
5+
6+
'use strict';
7+
8+
//------------------------------------------------------------------------------
9+
// Rule Definition
10+
//------------------------------------------------------------------------------
11+
12+
/** @type {import('eslint').Rule.RuleModule} */
13+
module.exports = {
14+
meta: {
15+
type: 'suggestion',
16+
docs: {
17+
description:
18+
"Disallow function calls in useState that aren't wrapped in an initializer function",
19+
recommended: false,
20+
url: null, // URL to the documentation page for this rule
21+
},
22+
fixable: null, // Or `code` or `whitespace`
23+
schema: [], // Add a schema if the rule has options
24+
messages: {
25+
useLazyInitialization:
26+
'To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: useState(() => getValue())',
27+
},
28+
},
29+
30+
// rule takes inspiration from https://github.com/facebook/react/issues/26520
31+
create(context) {
32+
// variables should be defined here
33+
const ALLOW_LIST = Object.freeze(['Boolean', 'String']);
34+
35+
//----------------------------------------------------------------------
36+
// Helpers
37+
//----------------------------------------------------------------------
38+
39+
// any helper functions should go here or else delete this section
40+
41+
const hasFunctionCall = (node) => {
42+
if (
43+
node.type === 'CallExpression'
44+
&& ALLOW_LIST.indexOf(node.callee.name) === -1
45+
) {
46+
return true;
47+
}
48+
if (node.type === 'ConditionalExpression') {
49+
return (
50+
hasFunctionCall(node.test)
51+
|| hasFunctionCall(node.consequent)
52+
|| hasFunctionCall(node.alternate)
53+
);
54+
}
55+
if (
56+
node.type === 'LogicalExpression'
57+
|| node.type === 'BinaryExpression'
58+
) {
59+
return hasFunctionCall(node.left) || hasFunctionCall(node.right);
60+
}
61+
return false;
62+
};
63+
64+
//----------------------------------------------------------------------
65+
// Public
66+
//----------------------------------------------------------------------
67+
68+
return {
69+
CallExpression(node) {
70+
// @ts-ignore
71+
if (node.callee && node.callee.name === 'useState') {
72+
if (node.arguments.length > 0) {
73+
const useStateInput = node.arguments[0];
74+
if (hasFunctionCall(useStateInput)) {
75+
context.report({ node, messageId: 'useLazyInitialization' });
76+
}
77+
}
78+
}
79+
},
80+
};
81+
},
82+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @fileoverview Detects function calls in useState and suggests using lazy initialization instead.
3+
* @author Patrick Gillespie
4+
*/
5+
6+
'use strict';
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const RuleTester = require('eslint').RuleTester;
13+
const rule = require('../../../lib/rules/prefer-use-state-lazy-initialization');
14+
15+
//------------------------------------------------------------------------------
16+
// Tests
17+
//------------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester();
20+
ruleTester.run('prefer-use-state-lazy-initialization', rule, {
21+
valid: [
22+
// give me some code that won't trigger a warning
23+
'useState()',
24+
'useState("")',
25+
'useState(true)',
26+
'useState(false)',
27+
'useState(null)',
28+
'useState(undefined)',
29+
'useState(1)',
30+
'useState("test")',
31+
'useState(value)',
32+
'useState(object.value)',
33+
'useState(1 || 2)',
34+
'useState(1 || 2 || 3 < 4)',
35+
'useState(1 && 2)',
36+
'useState(1 < 2)',
37+
'useState(1 < 2 ? 3 : 4)',
38+
'useState(1 == 2 ? 3 : 4)',
39+
'useState(1 === 2 ? 3 : 4)',
40+
],
41+
42+
invalid: [
43+
{
44+
code: 'useState(1 || getValue())',
45+
errors: [
46+
{
47+
message: rule.meta.messages.useLazyInitialization,
48+
type: 'CallExpression',
49+
},
50+
],
51+
},
52+
{
53+
code: 'useState(2 < getValue())',
54+
errors: [
55+
{
56+
message: rule.meta.messages.useLazyInitialization,
57+
type: 'CallExpression',
58+
},
59+
],
60+
},
61+
{
62+
code: 'useState(getValue())',
63+
errors: [
64+
{
65+
message: rule.meta.messages.useLazyInitialization,
66+
type: 'CallExpression',
67+
},
68+
],
69+
},
70+
{
71+
code: 'useState(getValue(1, 2, 3))',
72+
errors: [
73+
{
74+
message: rule.meta.messages.useLazyInitialization,
75+
type: 'CallExpression',
76+
},
77+
],
78+
},
79+
{
80+
code: 'useState(a ? b : c())',
81+
errors: [
82+
{
83+
message: rule.meta.messages.useLazyInitialization,
84+
type: 'CallExpression',
85+
},
86+
],
87+
},
88+
{
89+
code: 'useState(a() ? b : c)',
90+
errors: [
91+
{
92+
message: rule.meta.messages.useLazyInitialization,
93+
type: 'CallExpression',
94+
},
95+
],
96+
},
97+
{
98+
code: 'useState(a ? (b ? b1() : b2) : c)',
99+
errors: [
100+
{
101+
message: rule.meta.messages.useLazyInitialization,
102+
type: 'CallExpression',
103+
},
104+
],
105+
},
106+
{
107+
code: 'useState(a() && b)',
108+
errors: [
109+
{
110+
message: rule.meta.messages.useLazyInitialization,
111+
type: 'CallExpression',
112+
},
113+
],
114+
},
115+
{
116+
code: 'useState(a && b())',
117+
errors: [
118+
{
119+
message: rule.meta.messages.useLazyInitialization,
120+
type: 'CallExpression',
121+
},
122+
],
123+
},
124+
{
125+
code: 'useState(a() && b())',
126+
errors: [
127+
{
128+
message: rule.meta.messages.useLazyInitialization,
129+
type: 'CallExpression',
130+
},
131+
],
132+
},
133+
],
134+
});

0 commit comments

Comments
 (0)