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):
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.`,
});
}
},
},
);
};
}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
- React Docs: Nesting and organizing components
- React Docs: Passing props to a component
- React Docs:
no-component-hook-factoriesLint Rule
See Also
function-component-definition
Enforce arrow function syntax for function components.