Try @eslint-react/kit@beta
logoESLint React

@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/kit

Quick Start

eslint.config.ts
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 (functionComponentDefinitionfunction-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(): Builder

Creates 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/01KNE2WSJ8011D2HXE3A6H717C

To 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.

MethodDescription
useRegisters a rule factory. The rule name is kebabCase(factory.name). Options type is inferred from the factory signature.
getConfigReturns a Linter.Config with all registered rules enabled at "error" severity.
getPluginReturns 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.

eslint.config.ts
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[]): RuleListener

Merges 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 settings

collect

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.

MethodReturnsDescription
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:

QueryDescription
query.all(program)All collected semantic nodes in the file.

is

All predicates live under kit.is — organized into four sub-sections.

Component

PredicateSignatureDescription
componentDecl(node, hint) -> booleanWhether a function node is a component. (context pre-bound)
componentName(name) -> booleanStrict PascalCase component name check.
componentNameLoose(name) -> booleanLoose component name check.
componentWrapperCall(node) -> booleanWhether a node is a memo(…) or forwardRef(…) call. (context pre-bound)
componentWrapperCallback(node) -> booleanWhether a function is the callback passed to a wrapper. (context pre-bound)

Hook

General hook predicates:

PredicateSignatureDescription
hookDecl(node) -> booleanWhether a function node is a hook (by name).
hookCall(node) -> booleanWhether a node is a hook call.
hookName(name) -> booleanWhether a string matches the use[A-Z] convention.
useEffectLikeCall(node, additionalHooks?) -> booleanWhether a node is a useEffect/useLayoutEffect-like call.
useStateLikeCall(node, additionalHooks?) -> booleanWhether a node is a useState-like call.
useEffectSetupCallback(node) -> booleanWhether a node is a useEffect setup function.
useEffectCleanupCallback(node) -> booleanWhether a node is a useEffect cleanup function.

React API

Factory functions (context pre-bound):

PredicateSignatureDescription
API(apiName) -> (node) -> booleanFactory: creates a predicate for a React API identifier. (context pre-bound)
APICall(apiName) -> (node) -> booleanFactory: 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

PredicateSignatureDescription
APIFromReact(name, scope, importSource?) -> booleanWhether a variable comes from a React import.
APIFromReactNative(name, scope, importSource?) -> booleanWhether 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.forwardRef

Usage:

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.

MethodSignatureDescription
findParent(node, test) -> Node | nullWalks 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) -> NodeRecursively 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.

PropertyTypeDefaultDescription
versionstringauto-detectResolved React version (e.g. "19.2.4").
importSourcestring"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.
polymorphicPropNamestring | null"as"Prop name used for polymorphic components.
additionalStateHooksRegExpLikePattern matching custom hooks treated as state hooks.
additionalEffectHooksRegExpLikePattern 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.

eslint.config.ts

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().

eslint.config.ts

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.

On this page