Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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,
};