logoESLint React
Recipes

component-hook-factories

Disallows components or hooks defined inside other functions (factory pattern).

Overview

This recipe contains a custom rule that disallows defining components or hooks inside other functions (factory pattern). This is a simplified kit reimplementation of the built-in react-x/component-hook-factories rule.

Defining components or hooks inside other functions creates new instances on every call. React treats each as a completely different component, destroying and recreating the entire component tree, losing all state, and causing performance problems.

Rule Definition

Copy the following into your project (e.g. eslint.config.rules.ts):

eslint.config.rules.ts
import type { RuleDefinition } from "@eslint-react/kit";
import { merge } from "@eslint-react/kit";
import type { TSESTree } from "@typescript-eslint/utils";

/** Disallow defining components or hooks inside other functions (factory pattern). */
export function componentHookFactories(): RuleDefinition {
  function findParent(
    { parent }: TSESTree.Node,
    test: (n: TSESTree.Node) => boolean,
  ): TSESTree.Node | null {
    if (parent == null) return null;
    if (test(parent)) return parent;
    if (parent.type === "Program") return null;
    return findParent(parent, test);
  }

  function isFunction({ type }: TSESTree.Node) {
    return type === "FunctionDeclaration" || type === "FunctionExpression" || type === "ArrowFunctionExpression";
  }

  return (context, { collect }) => {
    const fc = collect.components(context);
    const hk = collect.hooks(context);
    return merge(
      fc.visitor,
      hk.visitor,
      {
        "Program:exit"(program) {
          const comps = fc.query.all(program);
          const hooks = hk.query.all(program);
          for (const { name, node, kind } of [...comps, ...hooks]) {
            if (name == null) continue;
            if (findParent(node, isFunction) == null) continue;
            context.report({
              node,
              message: `Don't define ${kind} "${name}" inside a function. Move it to the module level.`,
            });
          }
        },
      },
    );
  };
}
eslint.config.ts
import eslintReactKit from "@eslint-react/kit";
import { componentHookFactories } from "./eslint.config.rules";

export default [
  // ... other configs
  {
    ...eslintReactKit()
      .use(componentHookFactories)
      .getConfig(),
    files: ["src/**/*.tsx"],
  },
];

Invalid

// ❌ Factory function creating components
function createComponent(defaultValue) {
  return function Component() {
    // ...
  };
}
// ❌ Component defined inside another component
function Parent() {
  function Child() {
    return <div />;
  }

  return <Child />;
}
// ❌ Hook factory function
function createCustomHook(endpoint) {
  return function useData() {
    // ...
  };
}
// ❌ Hook defined inside a component
function MyComponent() {
  function useLocalState() {
    return useState(0);
  }
  // ...
}

Valid

// ✅ Component defined at module level
function Component({ defaultValue }) {
  // ...
}
// ✅ Custom hook at module level
function useData(endpoint) {
  // ...
}
// ✅ Pass props instead of using a factory
function Button({ color, children }) {
  return (
    <button style={{ backgroundColor: color }}>
      {children}
    </button>
  );
}

function App() {
  return (
    <>
      <Button color="red">Red</Button>
      <Button color="blue">Blue</Button>
    </>
  );
}

How It Works

This rule uses both the collect.components and collect.hooks collectors from @eslint-react/kit to find all component and hook definitions in the file. On Program:exit, it iterates through every detected component and hook and checks whether each one has a function ancestor (i.e. it is nested inside another function). If so, the definition is reported.

The findParent helper walks up the AST looking for a FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression ancestor. If one is found before reaching Program, the component or hook is considered nested and is flagged.

Further Reading


See Also

On this page