Skip to content

Instantly share code, notes, and snippets.

@dgcoffman
Created May 23, 2022 16:16
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dgcoffman/ec5014675ddfbd7b68929344234d9532 to your computer and use it in GitHub Desktop.
Save dgcoffman/ec5014675ddfbd7b68929344234d9532 to your computer and use it in GitHub Desktop.
libs/eslint-rules/require-named-effect.js
const isUseEffect = (node) => node.callee.name === 'useEffect';
const argumentIsArrowFunction = (node) => node.arguments[0].type === 'ArrowFunctionExpression';
const effectBodyIsSingleFunction = (node) => {
const { body } = node.arguments[0];
// It's a single unwrapped function call:
// `useEffect(() => theNameOfAFunction(), []);`
if (body.type === 'CallExpression') {
return true;
}
// There's a function body, but it just calls another function:
// `useEffect(() => {
// theOnlyChildIsAFunctionCall();
// }, []);`
if (body.body?.length === 1 && body.body[0]?.expression?.type === 'CallExpression') {
return true;
}
return false;
};
const fail = (report, node) => report(node, 'Complex effects must be a named function.');
module.exports = {
create(context) {
return {
CallExpression(node) {
if (
isUseEffect(node) &&
argumentIsArrowFunction(node) &&
!effectBodyIsSingleFunction(node)
) {
fail(context.report, node);
}
},
};
},
};
const rule = require('./require-named-effect');
const ruleTester = require('./helpers/ruleTester');
describe('require-named-effect', () => {
const valid = [
`useEffect(function namedFunction() {}, []);`,
`useEffect(theNameOfAFunction(), []);`,
`useEffect(() => theNameOfAFunction(), []);`,
`useEffect(() => {
theOnlyChildIsAFunctionCall();
}, []);`,
];
const invalid = [
`useEffect(() => {}, []);`,
`useEffect(() => {
const t = 1;
diallowTwoThings(t);
}, []);`,
];
ruleTester.run('require-named-effect', rule, {
valid,
invalid: invalid.map((code) => ({
code,
errors: ['Complex effects must be a named function.'],
})),
});
});
@mxdvl
Copy link

mxdvl commented May 24, 2022

Excellent!

Is it fair to say this was prompted by the following tweet: https://twitter.com/housecor/status/1528707302483697666 ?

@flucivja
Copy link

flucivja commented Dec 2, 2022

I would consider these as invalid too because

  • useEffect(theNameOfAFunction(), []); - it's not clear what dependencies should be used
  • useEffect(() => theNameOfAFunction(), []); - function theNameOfAFunction should be in dependencies by rules of hooks
  • useEffect(() => { theOnlyChildIsAFunctionCall(); }, []); - function theOnlyChildIsAFunctionCall should be in dependencies by rules of hooks

@lukasMega
Copy link

lukasMega commented Jan 16, 2023

If someone wants to very quickly add such a check for useEffect without integrating custom eslint plugin…
I created custom rule for 'no-restricted-syntax' rule for inspiration:

'no-restricted-syntax': [
// 1. case:
  {
    message: 'useEffect: please use named function instead of arrow function',
    selector:
        "CallExpression[callee.name='useEffect'][arguments.0.type='ArrowFunctionExpression'][arguments.0.body.body.length>1]",
  },

// 2. case (for single expression in useEffect's body - allow only single function calls and single assignments) 
  {
    message:
        'useEffect: please use named function instead of arrow function (also for single expression in body)',
    selector:
        "CallExpression[callee.name='useEffect'][arguments.0.type='ArrowFunctionExpression'][arguments.0.body.body.length=1][arguments.0.body.body.0.type='ExpressionStatement'][arguments.0.body.body.0.expression.type!='CallExpression'][arguments.0.body.body.0.expression.type!='AssignmentExpression']",
  },

// 3. case (for single statement in useEffect's body - allow only expression statement, return statement):
  {
    message:
        'useEffect: please use named function instead of arrow function (also for single statement in body)',
    selector:
        "CallExpression[callee.name='useEffect'][arguments.0.type='ArrowFunctionExpression'][arguments.0.body.body.length=1][arguments.0.body.body.0.type!='ExpressionStatement'][arguments.0.body.body.0.type!='ReturnStatement']",
  },

// 4. case (for IIFE as single expression inside useEffect's body):
  {
    message:
         'useEffect: please use named function instead of arrow function (also for single function expression in body)',
    selector:
        "CallExpression[callee.name='useEffect'][arguments.0.type='ArrowFunctionExpression'][arguments.0.body.body.length=1][arguments.0.body.body.0.type='ExpressionStatement'][arguments.0.body.body.0.expression.type='CallExpression'][arguments.0.body.body.0.expression.callee.type=/((FunctionExpression)|(ArrowFunctionExpression))/]",
  },
]
  1. case should hit all usages of useEffect with arrow function that contains more than 1 statement/expression in body:
useEffect(() => { // ❌ not allowed
    initSomeSdk(API_VERSION).then(() => setInitialized(true));
    initOtherSdk(API_VERSION).then(() => setOtherInitialized(true));
}, []);
useEffect(() => { // ❌ not allowed
    someRef.current = someValue;
    someOtherRef.current = otherValue;
}, []);
  1. case should hit all usages of useEffect with arrow function that contains exactly 1 expression in body, but except (function) call expression or assignment expression:
useEffect(() => { // ✅ ok
    initSomeSdk(API_VERSION).then(() => setInitialized(true));
}, []);
useEffect(() => { // ✅ ok
    someRef.current = someValue;
}, []);
  1. case should hit all usages of useEffect with arrow function that contains exactly 1 statement with one exception - return statement:
useEffect(() => { // ✅ ok
    return () => {
        onCloseRef.current?.();
    };
}, [onCloseRef]);

useEffect(() => {  // ❌ not allowed
    if (something) {
      
    }
}, []);

useEffect(() => {  // ❌ not allowed
    try {
       
    } catch (e) {
       
    }
}, []);
  1. case should hit all usages of useEffect with arrow function that contains 1 call expression in body and it is immediately invoked function expression (IIFE):
useEffect(() => { // ❌ not allowed
    (function fun() {
        // …
    })();
}, []);
useEffect(() => { // ❌ not allowed
    (() => {
        // …
    })();
}, []);

some useful resources for AST selectors if you want to edit these rules (selectors):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment