logoESLint React

Migrating from eslint-plugin-react

Complete guide for migrating from eslint-plugin-react to ESLint React

This guide provides a comprehensive comparison between eslint-plugin-react and ESLint React rules to help you migrate your existing configuration.

Overview

ESLint React is a collection of ESLint plugins and utilities for React, with a focus on performance, TypeScript integration, and alignment with react.dev guidelines. It offers a different set of rules and design choices compared to eslint-plugin-react.

Rule Comparison Table

Not all rules have direct equivalents, and some behave differently.

Legend

  • 🔧 Fully supported - Rule is supported, and has an auto-fix
  • 🟢 Mostly supported - Rule is supported but doesn't have an auto-fix
  • 🟡 Partial support - Similar but not identical functionality
  • 🔴 Not supported - No equivalent rule
  • ➡️ External plugin - Rule is available in another ESLint plugin
  • 🚫 Legacy - Rule is not applicable in modern TypeScript React development (e.g., class-based components, propTypes)
  • ⚠️ Warning - Rule has been deprecated in eslint-plugin-react
  • 🔄 Codemod - Rule is supported as a codemod (safe AST transformation)
  • 💭 Requires type checking - Rule requires TypeScript type-aware linting to function correctly
  • 🧰 Supported via @eslint-react/kit - Rule is supported as a custom rule via @eslint-react/kit

A variety of rules are marked legacy, but still have equivalent rules. This distinction was done to more accurately assess migration for React written with function components and no longer use propTypes.

Table

The following table compares all rules from eslint-plugin-react with their ESLint React (or external) equivalents:

eslint-plugin-react Rule NameNew Rule NamePrev StatusStatus
boolean-prop-naming@eslint-react/kit/boolean-prop-naming report-only🟢💭
button-has-typedom-no-missing-button-type🟢🔧
checked-requires-onchange-or-readonly@eslint-react/kit/checked-requires-onchange-or-readonly report-only🟢🧰
default-props-match-prop-types🟢🚫
destructuring-assignment@eslint-react/kit (custom rule)🔧🧰
display-nameno-missing-component-display-name + no-missing-context-display-name🟢🟡
forbid-component-props@eslint-react/kit/forbid-component-props report-only🟢🧰
forbid-dom-props@eslint-react/kit/forbid-dom-props report-only🟢🧰
forbid-elements@eslint-react/kit/forbid-elements report-only🟢🧰
forbid-foreign-prop-types🟢🚫
forbid-prop-types🟢🚫
forward-ref-uses-refno-forward-ref🟢🔄
function-component-definition@eslint-react/kit (custom rule, e.g. @eslint-react/kit/function-component-definition; report-only by default, autofix depends on your implementation)🟢🔧
hook-use-stateuse-state🟢🟢
iframe-missing-sandboxdom-no-missing-iframe-sandbox🟢🔧
jsx-boolean-value@eslint-react/kit/jsx-boolean-value includes autofix🔧🔧
jsx-child-element-spacing@stylistic/jsx-child-element-spacing🟢➡️
jsx-closing-bracket-location@stylistic/jsx-closing-bracket-location🔧➡️
jsx-closing-tag-location@stylistic/jsx-closing-tag-location🔧➡️
jsx-curly-brace-presence@stylistic/jsx-curly-brace-presence🔧➡️
jsx-curly-newline@stylistic/jsx-curly-newline🔧➡️
jsx-curly-spacing@stylistic/jsx-curly-spacing🔧➡️
jsx-equals-spacing@stylistic/jsx-equals-spacing🔧➡️
jsx-first-prop-new-line@stylistic/jsx-first-prop-new-line🔧➡️
jsx-fragments@eslint-react/kit/jsx-fragments🔧🔧
jsx-handler-names@eslint-react/kit/jsx-handler-names🟢🧰
jsx-indent@stylistic/jsx-indent🔧➡️
jsx-indent-props@stylistic/jsx-indent-props🔧➡️
jsx-keyno-missing-key + no-duplicate-key + no-implicit-key🟢🟢
jsx-max-depth@eslint-react/kit/jsx-max-depth🟢🧰
jsx-max-props-per-line@stylistic/jsx-max-props-per-line🔧➡️
jsx-newline@stylistic/jsx-newline🔧➡️
jsx-no-bind@eslint-react/kit/jsx-no-bind; report-only🟢🧰
jsx-no-comment-textnodesjsx-no-comment-textnodes🟢🟢
jsx-no-constructed-context-valuesno-unstable-context-value🟢🟢
jsx-no-duplicate-props@eslint-react/kit/jsx-no-duplicate-props🟢🧰
jsx-no-leaked-renderno-leaked-conditional-rendering🔧🟢
jsx-no-literals@eslint-react/kit/jsx-no-literals🟢🧰
jsx-no-script-urldom-no-script-url🟢🟢
jsx-no-target-blankdom-no-unsafe-target-blank🔧🟢
jsx-no-undefN/A (ESLint v10.0.0+ now tracks JSX references natively)🟢🔴
jsx-no-useless-fragmentjsx-no-useless-fragment🔧🟢
jsx-one-expression-per-line@stylistic/jsx-one-expression-per-line🔧➡️
jsx-pascal-case@eslint-react/kit/jsx-pascal-case🟢🧰
jsx-props-no-multi-spaces@stylistic/jsx-props-no-multi-spaces🔧➡️
jsx-props-no-spread-multi@eslint-react/kit/jsx-props-no-spread-multi🟢🧰
jsx-props-no-spreading@eslint-react/kit/jsx-props-no-spreading; report-only🟢🧰
jsx-sort-default-props⚠️
jsx-sort-props@stylistic/jsx-sort-props🔧➡️
jsx-space-before-closing⚠️
jsx-tag-spacing@stylistic/jsx-tag-spacing🔧➡️
jsx-uses-reactN/A (ESLint v10.0.0+ now tracks JSX references natively)🟢🔴
jsx-uses-varsN/A (ESLint v10.0.0+ now tracks JSX references natively)🟢🔴
jsx-wrap-multilines@stylistic/jsx-wrap-multilines🔧➡️
no-access-state-in-setstateno-access-state-in-setstate🟢🟢
no-adjacent-inline-elements@eslint-react/kit/no-adjacent-inline-elements🟢🧰
no-array-index-keyno-array-index-key🟢🟢
no-arrow-function-lifecycle🔧🚫
no-children-propjsx-no-children-prop🟢🟢
no-dangerdom-no-dangerously-set-innerhtml🟢🟢
no-danger-with-childrendom-no-dangerously-set-innerhtml-with-children🟢🟢
no-deprecatedno-component-will-mount + no-component-will-receive-props + no-component-will-update + no-create-ref + no-forward-ref + dom-no-render + dom-no-render-return-value + dom-no-hydrate + dom-no-find-dom-node🟢🟡
no-did-mount-set-stateno-set-state-in-component-did-mount🟢🚫
no-did-update-set-stateno-set-state-in-component-did-update🟢🚫
no-direct-mutation-stateno-direct-mutation-state🟢🚫
no-find-dom-nodedom-no-find-dom-node🟢🟢
no-invalid-html-attributeSee also dom-no-unknown-property🟢🔴
no-is-mounted🟢🚫
no-multi-comp@eslint-react/kit/no-multi-comp; report-only🟢🧰
no-namespacejsx-no-namespace🟢🟢
no-object-type-as-default-propno-unstable-default-props🟢🟢
no-render-return-valuedom-no-render-return-value🟢🟢
no-set-state@eslint-react/kit/no-set-state report-only🟢🧰
no-string-refs@eslint-react/kit/no-string-refs report-only🟢🧰
no-this-in-sfc🟢🚫
no-typos🟢🔴
no-unknown-propertydom-no-unknown-property🔧🔧
no-unsafeno-unsafe-component-will-mount + no-unsafe-component-will-receive-props + no-unsafe-component-will-update🟢🟡
no-unstable-nested-componentsno-nested-component-definitions🟢🟢
no-unused-class-component-methodsno-unused-class-component-members🟢🚫
no-unused-prop-typesno-unused-props🟢🚫
no-unused-stateno-unused-state🟢🟡
no-will-update-set-stateno-set-state-in-component-will-update🟢🟢
prefer-es6-class🟢🚫
prefer-exact-props🟢🚫
prefer-read-only-props🔧🚫
prefer-stateless-function🟢🚫
prop-types🟢🚫
react-in-jsx-scope🟢🚫
require-default-props🟢🚫
require-optimization🟢🚫
require-render-return🟢🚫
self-closing-comp@stylistic/jsx-self-closing-comp🔧➡️
sort-comp🟢🚫
sort-default-props🟢🚫
sort-prop-types🔧🚫
state-in-constructor🟢🚫
static-property-placement🟢🚫
style-prop-objectdom-no-string-style-prop🟢🟢
void-dom-elements-no-childrendom-no-void-elements-with-children🟢🟢

ESLint React Column

  • Rule names link to ESLint React documentation
  • Multiple rules separated by / indicate alternative approaches
  • Rules with + indicate multiple rules that together provide equivalent functionality

Gradual Migration

You can migrate gradually by using both plugins together, using the disable-conflict-eslint-plugin-react ruleset:

eslint.config.ts
import eslintReact from "@eslint-react/eslint-plugin";
import pluginReact from "eslint-plugin-react";
import { defineConfig } from "eslint/config";

export default defineConfig([
  // Start with the eslint-plugin-react
  {
    files: ["**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}"],
    extends: [
      // Whatever config you had enabled with eslint-plugin-react
      pluginReact.configs.flat.recommended,

      // Now disable all conflicting rules
      eslintReact.configs["disable-conflict-eslint-plugin-react"],

      // Now enable the desired rules
      eslintReact.configs["recommended-typescript"],
    ],
  },
]);

Once you have fully migrated, you can remove eslint-plugin-react entirely and rely solely on ESLint React:

eslint.config.ts
import eslintReact from "@eslint-react/eslint-plugin";
import { defineConfig } from "eslint/config";

export default defineConfig([
  {
    files: ["**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}"],
    extends: [
      eslintReact.configs["recommended-typescript"],
    ],
  },
]);

Custom Rules For Missing Rules

Some eslint-plugin-react rules don't have built-in equivalents in ESLint React, or you may want to customize their behavior. You can use @eslint-react/kit to create minimal custom rule implementations directly in your eslint.config.ts.

Install @eslint-react/kit first:

npm install --save-dev @eslint-react/kit

Below are drop-in rule definitions for the most commonly needed rules. Register them via the .use() chain in your config:

eslint.config.ts
import  from "@eslint-react/eslint-plugin";
import  from "@eslint-react/kit";
import {  } from "eslint/config";
import {
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
  ,
} from "@local/samples";

export default ([
  {
    : ["**/*.{ts,tsx}"],
    : [
      .["recommended-typescript"],
      ()
        .(, { : "^(is|has|should)[A-Z]([A-Za-z0-9]?)+" })
        .()
        .(, { : ["className", "style"] })
        .(, { : ["style", "className"] })
        .()
        .(, { : "handle", : "on" })
        .(, { : 3 })
        .()
        .(, { : true, : ["allowed"] })
        .()
        .()
        .()
        .()
        .()
        .(, {
          : new ([
            ["button", "Use <Button> from '@/components/ui' instead."],
            ["input", "Use <Input> from '@/components/ui' instead."],
          ]),
        })
        .()
        .()
        .()
        .()
        .(),
    ],
  },
]);

boolean-prop-naming

Enforce a naming convention for boolean props.

.config/booleanPropNaming.ts
import { type ,  } from "@eslint-react/kit";
import {  } from "@typescript-eslint/utils";
import ts from "typescript";

export type  = {
  /** A regular expression that boolean prop names must match. */
  ?: string;
};

const  = "^(is|has|should)[A-Z]([A-Za-z0-9]?)+";

/**
 * Flattens a union type into its constituent types
 * @param t The type to flatten
 * @returns The flattened types
 */
function (: ts.Type): ts.Type[] {
  return .() ? ..() : [];
}

/**
 * Checks whether the given type is boolean or a union containing boolean
 * @param t The type to check
 * @returns True if the type is boolean-like, false otherwise
 */
function (: ts.Type) {
  return ().(() => (.() & ts..) > 0);
}

/** Enforce boolean prop naming convention. */
export function (?: ):  {
  const {  =  } =  ?? {};
  const  = new ();

  return (, {  }) => {
    const  = .(, false);
    const  = ..();
    const { ,  } = .();

    return (, {
      "Program:exit"() {
        const  = .();

        for (const  of ) {
          const [] = ..;
          if ( == null) continue;

          const  = ..();
          const  = .();
          const  = .();

          for (const  of ) {
            const  = .(, );

            // Guard: must be a boolean-like type
            if (!()) continue;

            // Guard: must not already match the naming pattern
            if (.(.)) continue;

            // Guard: must have a declaration to report on
            const  = .();
            if ( == null || . === 0) continue;

            const [] = ;
            if ( == null) continue;

            // Report on the prop key when available
            const  = ..();
            const  = "key" in  ? . : ;
            .({
              : { : .,  },
              : `Boolean prop "{{name}}" should match "{{rule}}".`,
              ,
            });
          }
        }
      },
    });
  };
}

checked-requires-onchange-or-readonly

Require onChange or readOnly when using checked on <input>.

.config/checkedRequiresOnchangeOrReadonly.ts
import type {  } from "@eslint-react/kit";

/** Require `onChange` or `readOnly` when using `checked` on `<input>`. */
export function ():  {
  return () => {
    // Fast path: skip if `checked` is not present in the file
    if (!...("checked")) return {};

    return {
      () {
        // Guard: only check <input> elements
        const  = .. === "JSXIdentifier" ? .. : null;
        if ( !== "input") return;

        // Collect all attribute names
        const  = new <string>();
        for (const  of .) {
          if (. === "JSXAttribute" && .. === "JSXIdentifier") {
            .(..);
          }
        }

        // Guard: must have checked attribute
        if (!.("checked")) return;

        // Guard: satisfied by onChange or readOnly
        if (.("onChange") || .("readOnly")) return;

        .({
          : "`checked` requires `onChange` or `readOnly`.",
          ,
        });
      },
    };
  };
}

forbid-component-props

Forbid certain props on React components (not DOM elements). Only reports on PascalCase elements.

import type {  } from "@eslint-react/kit";

/** Options for {@link forbidComponentProps}. */
export type  = {
  /** Prop names that are not allowed on React components. */
  : string[];
};

/** Forbid certain props on React components (not DOM elements). */
export function (: ):  {
  const {  } = ;
  return () => ({
    () {
      // Extract prop name
      const  = .. === "JSXIdentifier" ? .. : null;
      if ( == null || !.()) return;

      // Extract element name
      const  = ... === "JSXIdentifier" ? ... : null;

      // Guard: only check components (PascalCase), not DOM elements
      if ( == null || [0] !== [0]?.()) return;

      .({
        : `Prop "${}" is forbidden on components.`,
        ,
      });
    },
  });
}

forbid-dom-props

Forbid certain props on DOM elements (not React components). Only reports on lowercase DOM element names.

import type {  } from "@eslint-react/kit";

/** Options for {@link forbidDomProps}. */
export type  = {
  /** Prop names that are not allowed on DOM elements. */
  : string[];
};

/** Forbid certain props on DOM elements (not React components). */
export function (: ):  {
  const {  } = ;
  return () => ({
    () {
      // Extract prop name
      const  = .. === "JSXIdentifier" ? .. : null;
      if ( == null || !.()) return;

      // Extract element name
      const  = ... === "JSXIdentifier" ? ... : null;

      // Guard: only check DOM elements (lowercase), not components
      if ( == null || [0] !== [0]?.()) return;

      .({
        : `Prop "${}" is forbidden on DOM elements.`,
        ,
      });
    },
  });
}

forbid-elements

Forbid specific JSX elements. Customize the forbidden map with your project's requirements.

import type {  } from "@eslint-react/kit";

/** Options for {@link forbidElements}. */
export type  = {
  /** A map from element name to the error message reported when that element is used. */
  : <string, string>;
};

/** Forbid specific JSX elements. */
export function (: ):  {
  const {  } = ;
  return () => ({
    () {
      // Extract element identifier
      const  = .. === "JSXIdentifier" ? .. : null;

      // Guard: must be in forbidden map
      if ( == null || !.()) return;

      // Report violation with custom message
      .({ : .()!,  });
    },
  });
}

function-component-definition

Enforce arrow function definitions for function components.

import { type RuleFix, type RuleFixer, type ,  } from "@eslint-react/kit";
import type {  } from "@typescript-eslint/types";

type  = <>[0];

/** Enforce arrow function definitions for function components. */
export function ():  {
  return (, { ,  }) => {
    const { ,  } = .(, {
      : .. & ~..,
    });
    return (
      ,
      {
        "Program:exit"() {
          for (const {  } of .()) {
            // Guard: must not already be an arrow function
            if (. === "ArrowFunctionExpression") continue;

            .({
              : "Function components must be defined with arrow functions.",
              ,
              : [
                {
                  : "Convert to arrow function.",
                  : (, ),
                },
              ],
            });
          }
        },
      },
    );
  };
}

/**
 * Builds a fix that converts a function component into an arrow function
 * @param context The rule context
 * @param node The component function node
 * @returns A fixer function that applies the conversion, or yields null when the node cannot be converted
 */
function (: , : . | .FunctionExpression) {
  return (: RuleFixer): RuleFix | null => {
    const  = .;
    // Guard: generator functions can't be converted to arrow functions
    if (.) return null;

    const  = . ? "async " : "";
    const  = . != null ? .(.) : "";
    const  = `(${..(() => .()).(", ")})`;
    const  = . != null ? .(.) : "";
    const  = .(.);

    // `function Foo() {}` -> `const Foo = () => {};`
    if (. === "FunctionDeclaration" && . != null) {
      // dprint-ignore
      return .(, `const ${..} = ${}${}${}${} => ${};`);
    }

    // `const Foo = function() {}` -> `const Foo = () => {};`
    if (. === "FunctionExpression" && .. === "VariableDeclarator") {
      // dprint-ignore
      return .(, `${}${}${}${} => ${}`);
    }

    // `{ Foo: function() {} }` -> `{ Foo: () => {} }`
    if (. === "FunctionExpression" && .. === "Property") {
      // dprint-ignore
      return .(., `${.(..)}: ${}${}${}${} => ${}`);
    }

    return null;
  };
}

jsx-boolean-value

Enforce shorthand for boolean JSX attributes (e.g. prefer <C disabled /> over <C disabled={true} />).

import type {  } from "@eslint-react/kit";

/** Enforce shorthand for boolean JSX attributes. */
export function ():  {
  return (, {  }) => ({
    () {
      const {  } = ;

      // Guard: must have expression value
      if (?. !== "JSXExpressionContainer") return;

      // Guard: must be literal true
      const  = .(.);
      if (. !== "Literal" || . !== true) return;

      // Report: prefer shorthand form
      .({
        : () => .([..[1], .[1]]),
        : "Omit the value for boolean attributes.",
        ,
      });
    },
  });
}

jsx-fragments

Enforce shorthand syntax for React fragments. Reports when <React.Fragment> is used instead of <>...</>. Allows standard form when key prop is present.

import type {  } from "@eslint-react/kit";
import type {  } from "@typescript-eslint/types";

/** Options for {@link jsxFragments}. */
export type  = {
  /** The mode to enforce: "syntax" (default, shorthand) or "element" (standard form). */
  ?: "syntax" | "element";
};

/** Enforce shorthand or standard form for React fragments. */
export function (:  = {}):  {
  const {  = "syntax" } = ;
  return () => {
    // Fast path: skip if the fragment form this mode targets is not present in the file
    const  =  === "syntax" ? "Fragment" : "<>";
    if (!...()) return {};

    /** Report a standard-form fragment (`<Fragment>` / `<React.Fragment>`) that should use shorthand syntax. */
    function (: .JSXOpeningElement, : "React.Fragment" | "Fragment") {
      // Guard: has key prop (legitimate use of standard form)
      const  = .. > 0;
      if () return;

      .({
        () {
          const  = ..;
          if ( == null) return null;
          return [.(, "<>"), .(, "</>")];
        },
        : `Use shorthand fragment syntax '<>...</>' instead of '<${}>...</${}'.`,
        ,
      });
    }

    return {
      () {
        if ( !== "element") return;

        .({
          () {
            return [
              .(., "<React.Fragment>"),
              .(., "</React.Fragment>"),
            ];
          },
          : "Use '<React.Fragment>...</React.Fragment>' instead of shorthand '<>...</>'.",
          ,
        });
      },
      () {
        const  = .;

        if (. === "JSXIdentifier") {
          if (. === "Fragment" &&  === "syntax") {
            (, "Fragment");
          }
          return;
        }

        // Guard: only `<React.Fragment>` member expressions are relevant below
        if (. !== "JSXMemberExpression") return;
        if (.. !== "JSXIdentifier" || .. !== "React") return;
        if (.. !== "Fragment") return;

        if ( === "syntax") {
          (, "React.Fragment");
        }
      },
    };
  };
}

jsx-handler-names

Enforce naming convention for JSX event handler props and the functions they reference.

import type {  } from "@eslint-react/kit";

/** Options for {@link jsxHandlerNames}. */
export type  = {
  /** Whether to check inline functions (default: false). */
  ?: boolean;
  /** Prefix for event handler functions (default: "handle"). */
  ?: string;
  /** Prefix for event handler props (default: "on"). */
  ?: string;
};

/** Enforce naming convention for JSX event handlers. */
export function (:  = {}):  {
  const {
     = false,
     = "handle",
     = "on",
  } = ;
  const  = new (`^${}[A-Z]`);
  const  = new (`^${}[A-Z]`);

  return (, {  }) => ({
    () {
      // Guard: must be event handler prop (onXxx)
      if (.. !== "JSXIdentifier") return;
      const  = ..;
      if (!.()) return;

      // Guard: must have an expression container value
      const  = .;
      if (?. !== "JSXExpressionContainer") return;

      const  = .(.);

      // Case: direct reference (onClick={handleClick})
      if (. === "Identifier") {
        const  = .;
        // Guard: handler name already follows the convention
        if (.()) return;
        .({
          : `Handler function "${}" should be named "${}${.(.)}..."`,
          : ,
        });
        return;
      }

      // Case: inline function (onClick={() => {}})
      if (. !== "ArrowFunctionExpression" && . !== "FunctionExpression") return;
      // Guard: inline functions are allowed unless checkInlineFunction is enabled
      if (!) return;
      .({
        : [
          `Inline function handlers are not allowed for "${}".`,
          `Extract it to a named "${}${.(.)}" function.`,
        ].(" "),
        : ,
      });
    },
  });
}

jsx-max-depth

Enforce a maximum depth for JSX elements.

import type {  } from "@eslint-react/kit";
import type {  } from "@typescript-eslint/types";

/** Options for {@link jsxMaxDepth}. */
export type  = {
  /** Maximum allowed depth for JSX elements. */
  : number;
};

/** Enforce JSX maximum depth. */
export function (: ):  {
  const {  } = ;
  return () => ({
    () {
      const  = ();

      // Guard: only report elements nested deeper than the limit
      if ( <= ) return;

      .({
        : `JSX element exceeds maximum depth of ${} (found ${}).`,
        ,
      });
    },
  });
}

function ({  }: .) {
  let  = 0;

  while ( != null && . !== "Program") {
    if (. === "JSXElement") {
      ++;
    }
     = .;
  }

  return ;
}

jsx-no-bind

Prevent arrow functions, function expressions, and .bind() in JSX props.

import type {  } from "@eslint-react/kit";

/** Prevent inline functions and `.bind()` in JSX props. */
export function ():  {
  return (, { ,  }) => ({
    () {
      const  = .;

      // Guard: must be expression container
      if (?. !== "JSXExpressionContainer") return;

      const  = .(.);

      // Report: inline function
      if (. === "ArrowFunctionExpression" || . === "FunctionExpression") {
        .({ : "JSX props should not use inline functions.",  });
        return;
      }

      // Report: .bind() call
      if (.("bind")()) {
        .({ : "JSX props should not use .bind().",  });
      }
    },
  });
}

jsx-no-duplicate-props

Disallow duplicate properties in JSX elements.

import type {  } from "@eslint-react/kit";

/** Options for {@link jsxNoDuplicateProps}. */
export type  = {
  /** Whether to ignore case when checking for duplicate props. */
  ?: boolean;
};

/** Disallow duplicate properties in JSX. */
export function (:  = {}):  {
  const {  = false } = ;
  return () => ({
    () {
      // Track prop names already seen on this element
      const  = new <string>();

      for (const  of .) {
        // Guard: only plain attributes with simple names can be duplicates
        if (. !== "JSXAttribute") continue;
        if (.. !== "JSXIdentifier") continue;

        const  =  ? ...() : ..;

        // Report: duplicate prop
        if (.()) {
          .({
            : `Duplicate prop "${..}" found.`,
            : ,
          });
          continue;
        }

        .();
      }
    },
  });
}

jsx-no-literals

Disallow usage of string literals in JSX. By default requires wrapping strings in JSX expressions {'TEXT'}.

import type {  } from "@eslint-react/kit";
import type {  } from "@typescript-eslint/types";

/** Options for {@link jsxNoLiterals}. */
export type  = {
  /** An array of unique string values that would otherwise warn, but will be ignored. */
  ?: string[];
  /** When `true` the rule ignores literals used in props. */
  ?: boolean;
  /** Enforces no string literals used as children, wrapped or unwrapped. */
  ?: boolean;
};

/** Disallow usage of string literals in JSX. */
export function (:  = {}):  {
  const {  = [],  = true,  = false } = ;
  const  = new ();

  return () => {
    /** Report a string literal or JSX text used as a child, honoring the `noStrings` option. */
    const  = (: . | .JSXText, : string) => {
      if () {
        .({
          : `String literals are not allowed as JSX children.`,
          ,
        });
        return;
      }
      .({
        : `String literals should be wrapped in JSX expression: {'${}'}`,
        ,
      });
    };

    return {
      () {
        // Guard: only string literals are relevant
        if (typeof . !== "string") return;
        const  = ..();
        // Guard: skip empty and allowed strings
        if ( === "" || .()) return;

        const  = .;

        // Case: string literal used as a prop value
        if (. === "JSXAttribute") {
          // Guard: literals in props are ignored unless ignoreProps is disabled
          if () return;
          .({
            : [
              "String literals are not allowed in JSX props.",
              `Use {'${}'} instead.`,
            ].(" "),
            ,
          });
          return;
        }

        // Guard: already wrapped in an expression container
        if (. === "JSXExpressionContainer") return;

        // Case: string literal used as a child of a JSX element or fragment
        if (. === "JSXElement" || . === "JSXFragment") {
          (, );
        }
      },

      () {
        const  = ..();
        // Guard: skip empty and allowed strings
        if ( === "" || .()) return;

        (, );
      },
    };
  };
}

jsx-pascal-case

Enforce PascalCase for user-defined JSX components. DOM elements like <div> are ignored.

import type {  } from "@eslint-react/kit";

/** Options for {@link jsxPascalCase}. */
export type  = {
  /** Allow all-uppercase component names like `<XML />`. */
  ?: boolean;
  /** Allow leading underscores in component names like `<_Component />`. */
  ?: boolean;
};

/** Matches a PascalCase identifier like `MyComponent`. */
const  = /^[A-Z][a-zA-Z0-9]*$/;

/** Enforce PascalCase for user-defined JSX components. */
export function (:  = {}):  {
  const {  = false,  = false } = ;
  return () => ({
    () {
      const  = .;

      // Guard: must be simple identifier
      if (. !== "JSXIdentifier") return;

      const  = .;

      if (.("_")) {
        if (!) {
          .({
            : `Component name "${}" should not start with an underscore.`,
            : ,
          });
        }
        return;
      }

      // Guard: ignore DOM elements (lowercase)
      const  = [0];
      if ( == null) return;
      if ( === .()) return;

      if ( === .()) {
        if (!) {
          .({
            : `Component name "${}" should use PascalCase, not all uppercase.`,
            : ,
          });
        }
        return;
      }

      if (!.()) {
        .({
          : `Component name "${}" should be in PascalCase.`,
          : ,
        });
      }
    },
  });
}

jsx-props-no-spread-multi

Disallow spreading the same expression multiple times in a JSX element.

import type {  } from "@eslint-react/kit";

/** Disallow JSX prop spreading the same identifier multiple times. */
export function ():  {
  return (, {  }) => ({
    () {
      const  = new <string>();

      for (const  of .) {
        // Guard: only spread attributes are relevant
        if (. !== "JSXSpreadAttribute") continue;

        // Use the identifier name for plain spreads, otherwise the source text as the key
        const  = .(.);
        const  = . === "Identifier" ? . : ..(.);

        // Report: duplicate spread
        if (.()) {
          .({
            : `Spreading the same expression "${}" multiple times is not allowed.`,
            : ,
          });
          continue;
        }

        .();
      }
    },
  });
}

jsx-props-no-spreading

Disallow JSX props spreading.

import type {  } from "@eslint-react/kit";

/** Disallow JSX props spreading. */
export function ():  {
  return () => ({
    () {
      .({
        : "Props spreading is not allowed.",
        ,
      });
    },
  });
}

no-adjacent-inline-elements

Disallow adjacent inline elements not separated by whitespace.

import type {  } from "@eslint-react/kit";
import type {  } from "@typescript-eslint/types";

/** Tag names of inline HTML elements. */
const  = new ([
  "a",
  "abbr",
  "acronym",
  "b",
  "bdi",
  "bdo",
  "big",
  "br",
  "cite",
  "code",
  "dfn",
  "em",
  "i",
  "img",
  "input",
  "kbd",
  "label",
  "map",
  "object",
  "q",
  "samp",
  "script",
  "select",
  "small",
  "span",
  "strong",
  "sub",
  "sup",
  "textarea",
  "time",
  "tt",
  "var",
]);

/** Disallow adjacent inline elements not separated by whitespace. */
export function ():  {
  return () => ({
    () {
      const  = .;

      // Check each pair of adjacent children
      for (let  = 0;  < . - 1; ++) {
        const  = ([]);
        const  = ([ + 1]);

        // Guard: both neighbors must be inline elements
        if ( == null ||  == null) continue;

        .({
          : `Adjacent inline elements "${}" and "${}" should be separated by whitespace.`,
          : []!,
        });
      }
    },
  });
}

/**
 * Get the tag name of a JSX child when it is an inline element with a simple identifier name
 * @param child The JSX child to inspect
 * @returns The lowercase tag name, or null when the child is not an inline element
 */
function (: .JSXElement["children"][number] | undefined) {
  if (?. !== "JSXElement") return null;

  const  = ..;
  if (. !== "JSXIdentifier") return null;

  return .(.) ? . : null;
}

destructuring-assignment

Enforce destructuring assignment for component props.

import type {  } from "@eslint-react/kit";
import {  } from "@eslint-react/kit";

/** Enforce destructuring assignment for component props. */
export function ():  {
  return (, {  }) => {
    const { ,  } = .();

    return (, {
      "Program:exit"() {
        for (const {  } of .()) {
          const [] = .;
          // Guard: only check components with a plain identifier props parameter (e.g. `props`, not destructuring)
          if ( == null) continue;
          if (. !== "Identifier") continue;

          const  = .;
          const  = ..()..(() => . === );
          const  = ?. ?? [];

          // Report every member access on the props object (e.g. `props.foo`)
          for (const  of ) {
            const {  } = .;
            if (. !== "MemberExpression") continue;
            .({
              : "Use destructuring assignment for component props.",
              : ,
            });
          }
        }
      },
    });
  };
}

no-set-state

Forbid this.setState() calls.

import type {  } from "@eslint-react/kit";

/** Forbid this.setState() calls. */
export function ():  {
  return (, {  }) => {
    // Fast path: skip if `setState` is not present in the file
    if (!...("setState")) return {};

    return {
      () {
        // Guard: only match `this.setState(...)` calls
        if (!.("this.setState")()) return;

        .({
          : "This use of 'this.setState()' is not allowed.",
          ,
        });
      },
    };
  };
}

no-string-refs

Disallow deprecated string refs.

import type {  } from "@eslint-react/kit";

/** Disallow deprecated string refs. */
export function ():  {
  return () => {
    // Fast path: skip if no `ref` attribute can be present in the file
    if (!...("ref")) return {};

    return {
      () {
        // Guard: only check `ref` attributes
        if (.. !== "JSXIdentifier" || .. !== "ref") {
          return;
        }

        // Guard: only string literal values are string refs
        const  = .;
        if (?. !== "Literal" || typeof . !== "string") {
          return;
        }

        .({
          : [
            "String refs are deprecated and should not be used.",
            "Use React.useRef() instead.",
          ].(" "),
          ,
        });
      },
    };
  };
}

no-multi-comp

Prevent defining more than one component per file.

import type {  } from "@eslint-react/kit";
import {  } from "@eslint-react/kit";

/** Prevent defining more than one component per file. */
export function ():  {
  return (, {  }) => {
    const { ,  } = .();
    return (, {
      "Program:exit"() {
        const  = .();

        // Report every component past the first one
        for (const { ,  } of .(1)) {
          .({
            : [
              "Declare only one component per file.",
              `Found extra component "${ ?? "anonymous"}".`,
            ].(" "),
            ,
          });
        }
      },
    });
  };
}

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.