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 designed as a modern replacement for eslint-plugin-react with improved performance, better TypeScript support, and more accurate rule implementations.
However, not all rules have direct equivalents, and some behave differently.
Rule Comparison Table
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 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:
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:
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/kitBelow are drop-in rule definitions for the most commonly needed rules. Register them via the .use() chain in your config:
import from "@eslint-react/eslint-plugin";
import from "@eslint-react/kit";
import { } from "eslint/config";
import {
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
} from "@examples/react-dom-with-custom-rules";
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.
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;
};
/** Enforce boolean prop naming convention. */
export function (?: ): {
const = "^(is|has|should)[A-Z]([A-Za-z0-9]?)+";
const { = } = ?? {};
const = new ();
function (: ts.Type): ts.Type[] {
return .() ? ..() : [];
}
function (: ts.Type) {
return ().( => (.() & ts..) > 0);
}
return (, { }) => {
const = .(, false);
const = ..();
const { , } = .();
return (, {
"Program:exit"() {
const = .();
// ─── Iterate Components ────────────────────────
for (const of ) {
const [] = ..;
if ( == null) continue;
const = ..();
const = .();
const = .();
// ─── Iterate Props ─────────────────────────────
for (const of ) {
const = .(, );
// › Filter: must be boolean
if (!()) continue;
// › Filter: must match naming pattern
if (.(.)) continue;
const = .();
if ( == null || . === 0) continue;
const [] = ;
if ( == null) continue;
const = ..();
if ( == null) continue;
const = "key" in ? . : ;
// › Report violation
.({
: { : ., },
: `Boolean prop "{{name}}" should match "{{rule}}".`,
,
});
}
}
},
});
};
}checked-requires-onchange-or-readonly
Require onChange or readOnly when using checked on <input>.
import type { } from "@eslint-react/kit";
/** Require `onChange` or `readOnly` when using `checked` on `<input>`. */
export function (): {
return () => ({
() {
// › Verify this is an <input> element
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;
// › Validate: requires onChange or readOnly
if (!.("onChange") && !.("readOnly")) {
.({
,
: "`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;
// › Verify context is JSX opening element
const = .;
if (?. !== "JSXOpeningElement") 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;
// › Verify context is JSX opening element
const = .;
if (?. !== "JSXOpeningElement") 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 { } from "@eslint-react/kit";
import { } from "@eslint-react/kit";
/** Enforce arrow function definitions for function components. */
export function (): {
return (, { , }) => {
const { , } = .(, {
: .. & ~..,
});
return (
,
{
"Program:exit"() {
// ─── Iterate all components ────────────────────
for (const { } of .()) {
// › Guard: must not already be arrow function
if (. === "ArrowFunctionExpression") continue;
.({
,
: "Function components must be defined with arrow functions.",
: [
{
: "Convert to arrow function.",
() {
const = .;
if (.) return null;
const = . ? "async " : "";
const = . ? .(.) : "";
const = `(${..(() => .()).(", ")})`;
const = . ? .(.) : "";
const = .(.);
// ─── Case: function declaration ──────────────
if (. === "FunctionDeclaration" && .) {
// dprint-ignore
return .(, `const ${..} = ${}${}${}${} => ${};`);
}
// ─── Case: function expression in variable ───
if (. === "FunctionExpression" && .. === "VariableDeclarator") {
// dprint-ignore
return .(, `${}${}${}${} => ${}`);
}
// ─── Case: object method shorthand ───────────
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
.({
,
: "Omit the value for boolean attributes.",
: () => .([..[1], .[1]]),
});
},
});
}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 () => {
// ── Helpers ─────────────────────────────────────
function (: .JSXOpeningElement, : "React.Fragment" | "Fragment") {
// › Guard: has key prop (legitimate use of standard form)
const = .. > 0;
if () return;
.({
,
: `Use shorthand fragment syntax '<>...</>' instead of '<${}>...</${}'.`,
() {
const = .?.;
if (!) return null;
return [.(, "<>"), .(, "</>")];
},
});
}
// ── Listeners ────────────────────────────────────
return {
() {
const = .;
// ─── Handle <Fragment> (JSXIdentifier) ───────
if (. === "JSXIdentifier" && . === "Fragment") {
if ( === "syntax") {
(, "Fragment");
}
return;
}
// ─── Handle <React.Fragment> (JSXMemberExpression) ─
if (. !== "JSXMemberExpression") return;
if (.. !== "JSXIdentifier" || .. !== "React") return;
if (.. !== "JSXIdentifier" || .. !== "Fragment") return;
if ( === "syntax") {
(, "React.Fragment");
}
},
() {
if ( === "element") {
.({
,
: "Use '<React.Fragment>...</React.Fragment>' instead of shorthand '<>...</>'.",
() {
return [
.(., "<React.Fragment>"),
.(., "</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 = {
/** Prefix for event handler functions (default: "handle"). */
?: string;
/** Prefix for event handler props (default: "on"). */
?: string;
/** Whether to check inline functions (default: false). */
?: boolean;
};
/** Enforce naming convention for JSX event handlers. */
export function (: = {}): {
const {
= "handle",
= "on",
= false,
} = ;
const = new (`^${}[A-Z]`);
const = new (`^${}[A-Z]`);
return (, { }) => ({
() {
// › Guard: must be event handler prop (onXxx)
if (.. !== "JSXIdentifier") return;
const = ..;
if (!.()) return;
const = .;
if (!) return;
// ─── Check expression value ────────────────────
if (. === "JSXExpressionContainer") {
const = .(.);
// Case: direct reference (onClick={handleClick})
if (. === "Identifier") {
const = .;
if (!.()) {
.({
: ,
: `Handler function "${}" should be named "${}${
.(.)
}..."`,
});
}
return;
}
// Case: inline function (onClick={() => {}})
if (. === "ArrowFunctionExpression" || . === "FunctionExpression") {
if () {
.({
: ,
:
`Inline function handlers are not allowed for "${}". Extract it to a named "${}${
.(.)
}" function.`,
});
}
return;
}
}
},
});
}jsx-max-depth
Enforce a maximum depth for JSX elements.
import type { } from "@eslint-react/kit";
/** Options for {@link jsxMaxDepth}. */
export type = {
/** Maximum allowed depth for JSX elements. */
: number;
};
/** Enforce JSX maximum depth. */
export function (: ): {
const { } = ;
return () => ({
() {
let = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let : any = .;
// ─── Walk up the tree ──────────────────────────
while () {
if (.type === "JSXElement") {
++;
}
= .parent;
}
// › Check depth limit
if ( > ) {
.({
,
: `JSX element exceeds maximum depth of ${} (found ${}).`,
});
}
},
});
}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 = .(.);
// ─── Detect forbidden patterns ─────────────────
switch (true) {
case . === "ArrowFunctionExpression":
case . === "FunctionExpression":
.({ , : "JSX props should not use inline functions." });
break;
case . === "CallExpression": {
const = .(.);
if (
. === "MemberExpression"
&& .. === "Identifier"
&& .. === "bind"
) {
.({ , : "JSX props should not use .bind()." });
}
break;
}
}
},
});
}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 () => ({
() {
const = new <string, string>();
// ─── Check each attribute ──────────────────────
for (const of .) {
if (. !== "JSXAttribute") continue;
if (.. !== "JSXIdentifier") continue;
const = ? ...() : ..;
// › Report duplicate
if (.()) {
.({
: ,
: `Duplicate prop "${..}" found.`,
});
} else {
.(, ..);
}
}
},
});
}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";
/** Options for {@link jsxNoLiterals}. */
export type = {
/** Enforces no string literals used as children, wrapped or unwrapped. */
?: boolean;
/** 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;
};
/** Disallow usage of string literals in JSX. */
export function (: = {}): {
const { = false, = [], = true } = ;
const = new ();
return () => ({
// ─── Check literal text children ───────────────
() {
if (typeof . !== "string") return;
const = ..();
if ( === "" || .()) return;
const = .;
if (!) return;
// ─── Case: prop value ────────────────────────
if (. === "JSXAttribute") {
if (!) {
.({
,
: `String literals are not allowed in JSX props. Use {'${}'} instead.`,
});
}
return;
}
// ─── Case: already wrapped ───────────────────
if (. === "JSXExpressionContainer") return;
// ─── Case: child of element/fragment ─────────
if (. === "JSXElement" || . === "JSXFragment") {
if () {
.({
,
: `String literals are not allowed as JSX children.`,
});
} else {
.({
,
: `String literals should be wrapped in JSX expression: {'${}'}`,
});
}
}
},
// ─── Check JSX text nodes ──────────────────────
() {
const = ..();
if ( === "" || .()) return;
if () {
.({
,
: `String literals are not allowed as JSX children.`,
});
} else {
.({
,
: `String literals should be wrapped in JSX expression: {'${}'}`,
});
}
},
});
}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;
};
/** Enforce PascalCase for user-defined JSX components. */
export function (: = {}): {
const { = false, = false } = ;
const = /^[A-Z][a-zA-Z0-9]*$/;
return () => ({
() {
const = .;
// › Guard: must be simple identifier
if (. !== "JSXIdentifier") return;
const = .;
// ─── Handle leading underscore ───────────────
if (.("_")) {
if (!) {
.({
: ,
: `Component name "${}" should not start with an underscore.`,
});
}
return;
}
// › Guard: ignore DOM elements (lowercase)
const = [0];
if ( === ) return;
if ( === .()) return;
// ─── Handle all-caps ─────────────────────────
if ( === .()) {
if (!) {
.({
: ,
: `Component name "${}" should use PascalCase, not all uppercase.`,
});
}
return;
}
// ─── Validate PascalCase ─────────────────────
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>();
// ─── Check each spread attribute ───────────────
for (const of .) {
if (. !== "JSXSpreadAttribute") continue;
// › Extract spread identifier name
const = .(.);
let : string;
if (. === "Identifier") {
= .;
} else {
= ..(.);
}
// › Report duplicate spread
if (.()) {
.({
: ,
: `Spreading the same expression "${}" multiple times is not allowed.`,
});
} else {
.();
}
}
},
});
}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";
/** Disallow adjacent inline elements not separated by whitespace. */
export function (): {
/** Set 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",
]);
return () => ({
() {
const = .;
// ─── Check adjacent pairs ──────────────────────
for (let = 0; < . - 1; ++) {
const = [];
const = [ + 1];
// › Validate first element
if (?. !== "JSXElement") continue;
if (... !== "JSXIdentifier") continue;
const = ...;
if (!.()) continue;
// › Validate second element
if (?. !== "JSXElement") continue;
if (... !== "JSXIdentifier") continue;
const = ...;
if (!.()) continue;
// › Report violation
.({
: ,
: `Adjacent inline elements "${}" and "${}" should be separated by whitespace.`,
});
}
},
});
}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 [] = .;
if ( == null) continue;
if (. !== "Identifier") continue;
const = .;
const = ..()..(() => . === );
const = ?. ?? [];
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 (, { }) => ({
() {
const = .(.);
// Check if callee is this.setState
if (
. === "MemberExpression" &&
.. === "ThisExpression" &&
.. === "Identifier" &&
.. === "setState"
) {
.({
,
: "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 () => ({
() {
// Check if this is a ref attribute
if (.. !== "JSXIdentifier" || .. !== "ref") {
return;
}
// Check if the value is a string literal (string ref)
const = .;
if (?. === "Literal" && typeof . === "string") {
.({
,
: `String refs are deprecated and should not be used. Use callback refs or React.createRef() 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 excess components ──────────────────
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.