@eslint-react/kit
ESLint React's toolkit for building custom React rules with JavaScript functions
This module is currently in beta. APIs may change in future releases.
Installation
npm install --save-dev @eslint-react/kitQuick Start
import from "@eslint-react/eslint-plugin";
import , { } from "@eslint-react/kit";
import type { } from "@eslint-react/kit";
import from "@eslint/js";
import { } from "eslint/config";
import from "typescript-eslint";
/** Enforce function declarations for function components. */
function (): {
return (, { }) => {
const { , } = .();
return (
,
{
"Program:exit"() {
for (const { } of .()) {
if (. === "FunctionDeclaration") continue;
.({
,
: "Function components must be defined with function declarations.",
});
}
},
},
);
};
}
export default (
{
: ["**/*.{ts,tsx}"],
: [
..,
..,
.["recommended-typescript"],
()
.()
.(),
],
},
);The rule name is derived automatically from the function name (functionComponentDefinition → function-component-definition), and registered as @eslint-react/kit/function-component-definition at "error" severity.
API Reference
eslintReactKit (default export)
import eslintReactKit from "@eslint-react/kit";
eslintReactKit(): BuilderCreates a Builder instance for registering custom rules via the chainable .use() API.
RuleFunction
import type { RuleFunction } from "@eslint-react/kit";
type RuleFunction = (context: RuleContext, toolkit: RuleToolkit) => RuleListener;A function that receives the ESLint rule context and the structured RuleToolkit toolkit, and returns a RuleListener (AST visitor object).
Rules are defined as named functions that return a RuleFunction. The function name is automatically converted to kebab-case and used as the rule name under the @eslint-react/kit plugin namespace.
// Function name `noForwardRef` → rule name `no-forward-ref`
// Registered as `@eslint-react/kit/no-forward-ref`
function noForwardRef(): RuleFunction {
return (context, { is }) => ({ ... });
}
// Functions that accept options work the same way
function forbidElements({ forbidden }: ForbidElementsOptions): RuleFunction {
return (context) => ({ ... });
}Anonymous Rules
When you use an anonymous function (arrow function without a name) with .use(), a random hex string is automatically generated as the rule name:
// Registered as `@eslint-react/kit/a1b2c3d4e5f67890`
eslintReactKit().use(() => (context) => ({
CallExpression(node) {
// Critical check that cannot be easily disabled
},
}));Anonymous rules are ideal for checks that are critical to code quality or security and should never be bypassed via disable comments:
// This critical security check cannot be easily disabled by developers
eslintReactKit().use(() => (context) => ({
Property(node) {
// Prevent direct construction of '__html' objects in product code
if (node.key.type === "Identifier" && node.key.name === "__html") {
context.report({
node,
message: "Do not construct '__html' objects directly. Use a sanitization library or receive them from server-side code.",
});
}
},
}));Note: Since the rule name is random and changes on every ESLint run, developers cannot use standard rules configs or disable comments like:
// This will NOT work - the rule name is random!
{ rules: { "@eslint-react/kit/01KNE2WSJ8011D2HXE3A6H717C": "off" } }
// This will NOT work - the rule name is random!
// eslint-disable-next-line @eslint-react/kit/01KNE2WSJ8011D2HXE3A6H717CTo disable an anonymous rule, developers must modify the ESLint configuration file directly, which provides an audit trail for policy violations.
Builder
interface Builder {
use<F extends (...args: any[]) => RuleFunction>(factory: F, ...args: Parameters<F>): Builder;
getConfig(): Linter.Config;
getPlugin(): ESLint.Plugin;
}A chainable builder for registering custom rules.
| Method | Description |
|---|---|
use | Registers a rule factory. The rule name is kebabCase(factory.name). Options type is inferred from the factory signature. |
getConfig | Returns a Linter.Config with all registered rules enabled at "error" severity. |
getPlugin | Returns an ESLint.Plugin containing the registered rules and plugin metadata. |
getConfig
Returns a flat Linter.Config object with all registered rules set to "error". This is a convenience wrapper that calls getPlugin() internally and adds the plugin plus rule entries to the config.
eslintReactKit()
.use(noForwardRef) // no-arg factory
.use(version, "19") // factory with inferred options
.getConfig();getPlugin
Returns an ESLint.Plugin object containing the registered rules and plugin metadata (name and version). Use this when you need finer-grained control over how the plugin is integrated into your ESLint configuration.
const kit = eslintReactKit()
.use(noForwardRef)
.use(version, "19");
// Retrieve the raw plugin object
const plugin = kit.getPlugin();
// Use it in a custom flat config with your own namespace and severity
export default [
{
files: ["**/*.{ts,tsx}"],
plugins: {
react: plugin,
},
rules: {
"react/version": "error",
"react/no-forward-ref": "error",
},
},
];merge
import { merge } from "@eslint-react/kit";
merge(...listeners: RuleListener[]): RuleListenerMerges multiple RuleListener (visitor) objects into a single listener. When two or more listeners define the same visitor key, the handlers are chained and execute in order.
This is essential for combining a collector's visitor with your own inspection logic.
RuleToolkit — the toolkit object
The second argument passed to the RuleFunction function is a structured RuleToolkit object:
kit
├── collect -> Semantic collectors (components, hooks)
├── is -> All predicates (component, hook, React API, import source)
├── ast -> AST utilities (unwrap type expressions)
├── hint -> Detection hint bit-flags
├── flag -> Component characteristic bit-flags
├── settings -> Normalized ESLint React settingscollect
Collector factories create a { query, visitor } pair. The visitor must be merged into your rule listener via merge(). After traversal completes, query.all(program) yields all detected semantic nodes.
| Method | Returns | Description |
|---|---|---|
components(context, options?) | CollectorWithContext<FunctionComponentSemanticNode> | Detects function components. Options: { hint?: bigint, collectDisplayName?: boolean } |
hooks(context) | CollectorWithContext<HookSemanticNode> | Detects custom hook definitions. |
CollectorWithContext extends Collector with contextual queries:
| Query | Description |
|---|---|
query.all(program) | All collected semantic nodes in the file. |
is
All predicates live under kit.is — organized into four sub-sections.
Component
| Predicate | Signature | Description |
|---|---|---|
componentDecl | (node, hint) -> boolean | Whether a function node is a component. (context pre-bound) |
componentName | (name) -> boolean | Strict PascalCase component name check. |
componentNameLoose | (name) -> boolean | Loose component name check. |
componentWrapperCall | (node) -> boolean | Whether a node is a memo(…) or forwardRef(…) call. (context pre-bound) |
componentWrapperCallback | (node) -> boolean | Whether a function is the callback passed to a wrapper. (context pre-bound) |
Hook
General hook predicates:
| Predicate | Signature | Description |
|---|---|---|
hookDecl | (node) -> boolean | Whether a function node is a hook (by name). |
hookCall | (node) -> boolean | Whether a node is a hook call. |
hookName | (name) -> boolean | Whether a string matches the use[A-Z] convention. |
useEffectLikeCall | (node, additionalHooks?) -> boolean | Whether a node is a useEffect/useLayoutEffect-like call. |
useStateLikeCall | (node, additionalHooks?) -> boolean | Whether a node is a useState-like call. |
useEffectSetupCallback | (node) -> boolean | Whether a node is a useEffect setup function. |
useEffectCleanupCallback | (node) -> boolean | Whether a node is a useEffect cleanup function. |
React API
Factory functions (context pre-bound):
| Predicate | Signature | Description |
|---|---|---|
API | (apiName) -> (node) -> boolean | Factory: creates a predicate for a React API identifier. (context pre-bound) |
APICall | (apiName) -> (node) -> boolean | Factory: creates a predicate for a React API call. (context pre-bound) |
All React API predicates and factories have context pre-bound — no need to pass the rule context manually:
// Direct check
is.memoCall(node);
// Useful in filter/find
nodes.filter(is.memoCall);
// Factory for any API name
const isCreateRefCall = is.APICall("createRef");
isCreateRefCall(node);Import source
| Predicate | Signature | Description |
|---|---|---|
APIFromReact | (name, scope, importSource?) -> boolean | Whether a variable comes from a React import. |
APIFromReactNative | (name, scope, importSource?) -> boolean | Whether a variable comes from a React Native import. |
hint
Bit-flags that control what the component collector considers a "component". Combine with bitwise OR (|) and remove with bitwise AND-NOT (& ~).
// The default hint used when none is specified
hint.component.Default;
// All available flags
hint.component.DoNotIncludeFunctionDefinedAsObjectMethod;
hint.component.DoNotIncludeFunctionDefinedAsClassMethod;
hint.component.DoNotIncludeFunctionDefinedAsArrayMapCallback;
hint.component.DoNotIncludeFunctionDefinedAsArbitraryCallExpressionCallback;
// … and more (inherits all JsxDetectionHint flags)Customization example:
const { query, visitor } = collect.components(context, {
// Also treat object methods as components (remove the exclusion flag)
hint: hint.component.Default & ~hint.component.DoNotIncludeFunctionDefinedAsObjectMethod,
});flag
Bit-flags indicating component characteristics. Check with bitwise AND (&).
flag.component.None; // 0n — no flags
flag.component.Memo; // wrapped in React.memo
flag.component.ForwardRef; // wrapped in React.forwardRefUsage:
for (const component of query.all(program)) {
if (component.flag & flag.component.Memo) {
// This component is memoized
}
}ast
Low-level AST utilities for handling TypeScript-specific syntax.
| Method | Signature | Description |
|---|---|---|
findParent | (node, test) -> Node | null | Walks up the AST from the given node and returns the first ancestor that satisfies the predicate, stopping at Program. Returns null if no match is found. |
unwrap | (node) -> Node | Recursively strips TypeScript type-expression wrappers (TSAsExpression, TSSatisfiesExpression, TSNonNullExpression, TSTypeAssertion, TSInstantiationExpression) and ChainExpression from a node, returning the underlying value. |
ast.unwrap is useful when you need to inspect a callee, argument, or initializer that may be wrapped in a TypeScript type assertion:
import type { } from "@eslint-react/kit";
function (): {
return (, { }) => ({
() {
const = .(.);
if (
. === "MemberExpression"
&& .. === "Identifier"
&& .. === "window"
&& .. === "Identifier"
&& .. === "addEventListener"
) {
// Report leaked event listener…
}
},
});
}Without unwrap, the code above would miss calls like (window.addEventListener as any)("click", handler) because node.callee.type would be TSAsExpression instead of MemberExpression.
settings
Exposes the normalized react-x settings from the ESLint shared configuration (context.settings["react-x"]). This lets your custom rules read and react to the same project-level settings used by the built-in rules.
| Property | Type | Default | Description |
|---|---|---|---|
version | string | auto-detect | Resolved React version (e.g. "19.2.4"). |
importSource | string | "react" | The module React is imported from (e.g. "@pika/react"). |
compilationMode | "infer" | "annotation" | "syntax" | "all" | "off" | "off" | The React Compiler compilation mode the project uses. |
polymorphicPropName | string | null | "as" | Prop name used for polymorphic components. |
additionalStateHooks | RegExpLike | — | Pattern matching custom hooks treated as state hooks. |
additionalEffectHooks | RegExpLike | — | Pattern matching custom hooks treated as effect hooks. |
RegExpLike is an object with a test(s: string) => boolean method (same interface as RegExp).
Usage:
import type { } from "@eslint-react/kit";
function ( = "19"): {
return (, { }) => ({
() {
if (!..(`${}.`)) {
.({
: ,
: `This project requires React ${}, but detected version ${.}.`,
});
}
},
});
}Examples
Simple: Ban forwardRef
This is a simplified kit reimplementation of the built-in react-x/no-forward-ref rule.
import type { } from "@eslint-react/kit";
import , { } from "@eslint-react/kit";
function (): {
return (, { }) => ({
() {
if (.()) {
.({ , : "forwardRef is deprecated in React 19. Pass ref as a prop instead." });
}
},
});
}
// Usage
()
.()
.();Component: Destructure component props
This is a simplified kit reimplementation that enforces destructuring assignment for component props.
import type { } from "@eslint-react/kit";
import , { } from "@eslint-react/kit";
function (): {
return (, { }) => {
const { , } = .();
return (, {
"Program:exit"() {
for (const { } of .()) {
const [] = .;
if ( == null) continue;
if (. !== "Identifier") continue;
const = .;
const = ..()..(() => . === );
const = ?. ?? [];
for (const of ) {
const { } = .;
if (. !== "MemberExpression") continue;
.({
: "Use destructuring assignment for component props.",
: ,
});
}
}
},
});
};
}
// Usage
()
.()
.();Hooks: Warn on custom hooks that don't call other hooks
This is a simplified kit reimplementation of the built-in react-x/no-unnecessary-use-prefix rule.
import type { } from "@eslint-react/kit";
import , { } from "@eslint-react/kit";
function (): {
return (, { }) => {
const { , } = .();
return (, {
"Program:exit"() {
for (const { , } of .()) {
if (. === 0) {
.({
,
: "A custom hook should use at least one hook, otherwise it's just a regular function.",
});
}
}
},
});
};
}
// Usage
()
.()
.();Multiple Collectors: Error Boundaries
Validates usage of Error Boundaries instead of try/catch for errors in child components or the use hook.
This is a simplified kit reimplementation of the built-in react-x/error-boundaries rule.
import , { type , } from "@eslint-react/kit";
import type { } from "@typescript-eslint/types";
function (: . | null): boolean {
if ( == null) return false;
return . === "JSXElement" || . === "JSXFragment";
}
/** Validates usage of Error Boundaries instead of try/catch for errors in child components. */
export function (): {
return (, { , , }) => {
// Fast path: skip if `try` is not present in the file
if (!...("try")) return {};
const = .();
const = .();
const = new <.TryStatement>();
const = new <.CallExpression>();
return (
.,
.,
{
() {
if (!.()) return;
.();
},
"Program:exit"() {
const = ..();
const = ..();
const = [..., ...];
for (const of ) {
const = .(, () => . === "TryStatement");
const = .(, () => .(() => . === ));
if ( != null && != null && !.()) {
.({
: ,
:
"Use an Error Boundary instead of try/catch around the 'use' hook. The 'use' hook suspends the component, and its errors can only be caught by Error Boundaries.",
});
.();
}
}
for (const { } of ) {
for (const of ) {
if ( == null) continue;
if (!()) continue;
const = .(, () => . === "TryStatement");
if ( != null && !.()) {
.({
: ,
:
"Use an Error Boundary to catch errors in child components. Try/catch can't catch errors during React's rendering process.",
});
.();
}
}
}
},
},
);
};
}
// Usage
()
.()
.();Override Config: Using spread syntax with getConfig
getConfig() returns a plain config object. You can spread it into a new object to override or supplement its properties — for example, to scope the config to specific files or add extra settings alongside your kit rules.
import from "@eslint-react/kit";
// Spread the config into a new object to add or override properties like `files`:
export default [
{
...()
.()
.(, "19")
.(),
// Override `name` so it shows your own label in config inspector tools
: "react-custom-rules",
// Override `files` to scope the kit rules to specific source files
: ["src/**/*.ts", "src/**/*.tsx"],
},
];This pattern is especially useful when composing kit configs alongside other ESLint configs (e.g. typescript-eslint, eslint-plugin-react-hooks) via defineConfig — you can nest the spread object inside extends arrays and attach shared properties like files or languageOptions at the same level.
Advanced Config: Using getPlugin for custom plugin namespace
Use getPlugin() when you want full control over the plugin namespace and rule severities instead of the all-in-one getConfig().
import from "@eslint-react/kit";
const = ()
.()
.(, "19");
// Instead of kit.getConfig(), use kit.getPlugin() for full control:
const = .();
export default [
{
: ["**/*.{ts,tsx}"],
: {
// Choose your own namespace
"react-custom-rules": ,
},
: {
// Set individual severities
"react-custom-rules/no-forward-ref": "error",
"react-custom-rules/version": "error",
},
},
];Resources
- AST Explorer - A tool for exploring the abstract syntax tree (AST) of JavaScript code, which is essential for writing custom rules.
- ESLint Developer Guide - Official ESLint documentation for creating custom rules.
- Using the TypeScript Compiler API - TypeScript compiler API documentation for working with type information in custom rules.