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

ESLint rule for detecting function calls inside useState (for recommending lazy initialization) #26822

Closed
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
3 changes: 2 additions & 1 deletion packages/eslint-plugin-react-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ If you want more fine-grained configuration, you can instead add a snippet like
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
"react-hooks/exhaustive-deps": "warn",
"react-hooks/prefer-use-state-lazy-initialization": "warn"
}
}
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
'use strict';

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

const RuleTester = require('eslint').RuleTester;
const rule = require('../src/PreferUseStateLazyInitialization');

//------------------------------------------------------------------------------
// 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())',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you should get two errors. Because you have
two function calls.

errors: [
{
message: rule.meta.messages.useLazyInitialization,
type: 'CallExpression',
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'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,
},
fixable: null,
schema: [],
messages: {
useLazyInitialization:
'To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: useState(() => getValue())',
},
},

create(context) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think code will be clearer if you just ban all calls inside useState

Suggested change
create(context) {
create: (context) => {
let useStateCallExpression = null;
return {
CallExpression(node) {
if (node.callee.type === 'Identifier' && node.callee.name === 'useState') {
useStateCallExpression = node;
return;
}
if (useStateCallExpression) {
context.report({ node, messageId: 'useLazyInitialization' });
}
},
'CallExpression:exit': function (node) {
if (node === useStateCallExpression) {
useStateCallExpression = null;
}
},
};
},

const ALLOW_LIST = Object.freeze(['Boolean', 'String']);

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

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) {
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'});
}
}
}
},
};
},
};
2 changes: 2 additions & 0 deletions packages/eslint-plugin-react-hooks/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import RulesOfHooks from './RulesOfHooks';
import ExhaustiveDeps from './ExhaustiveDeps';
import PreferUseStateLazyInitialization from './PreferUseStateLazyInitialization';

export const configs = {
recommended: {
Expand All @@ -23,4 +24,5 @@ export const configs = {
export const rules = {
'rules-of-hooks': RulesOfHooks,
'exhaustive-deps': ExhaustiveDeps,
'prefer-use-state-lazy-initialization': PreferUseStateLazyInitialization,
};