React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

eslintrulesreacttypescriptaccessibility

Essential ESLint Rules

Configure the most impactful ESLint rules for React, hooks, TypeScript, imports, and accessibility.

Recipe

Quick-reference recipe card — copy-paste ready.

// Severity levels
"off"   // 0 — disable the rule
"warn"  // 1 — yellow warning, does not fail CI
"error" // 2 — red error, fails CI and blocks builds
 
// Common pattern: override a rule
{
  rules: {
    "rule-name": "error",
    "rule-name": ["error", { option: "value" }],
  },
}

When to reach for this: When the default presets are too loose or too strict and you need to fine-tune specific rules.

Working Example

// eslint.config.mjs
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
 
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
 
const compat = new FlatCompat({ baseDirectory: __dirname });
 
const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    rules: {
      // --- React rules ---
      "react/jsx-no-target-blank": "error",
      "react/no-unescaped-entities": "off",
      "react/self-closing-comp": "warn",
      "react/jsx-curly-brace-presence": [
        "warn",
        { props: "never", children: "never" },
      ],
 
      // --- Hooks rules ---
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn",
 
      // --- TypeScript rules ---
      "@typescript-eslint/no-unused-vars": [
        "error",
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
          caughtErrorsIgnorePattern: "^_",
        },
      ],
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/consistent-type-imports": [
        "error",
        { prefer: "type-imports" },
      ],
 
      // --- Import rules ---
      "import/order": [
        "warn",
        {
          groups: [
            "builtin",
            "external",
            "internal",
            ["parent", "sibling"],
            "index",
            "type",
          ],
          "newlines-between": "always",
          alphabetize: { order: "asc", caseInsensitive: true },
        },
      ],
      "import/no-duplicates": "error",
 
      // --- Accessibility rules ---
      "jsx-a11y/alt-text": "error",
      "jsx-a11y/anchor-is-valid": "warn",
    },
  },
];
 
export default eslintConfig;

What this demonstrates:

  • Rules organized by category: React, hooks, TypeScript, imports, accessibility
  • Mix of error (must fix) and warn (should fix) severities
  • Pattern-based ignoring of unused variables prefixed with _
  • Import ordering with group separation and alphabetization

Deep Dive

How It Works

  • Each rule has a severity level: "off", "warn", or "error"
  • Rules can accept options as the second element of an array: ["error", { option: true }]
  • The next/core-web-vitals preset already enables many rules — you override them in your own config
  • Rules from plugins are prefixed with the plugin name (e.g., react/, @typescript-eslint/)

Variations

React rules worth knowing:

RuleWhat It Catches
react/jsx-no-target-blankMissing rel="noreferrer" on target="_blank" links
react/no-unescaped-entitiesUnescaped ' or " in JSX text
react/self-closing-comp<div></div> instead of <div /> for empty elements
react/jsx-curly-brace-presenceUnnecessary {"string"} instead of string in JSX
react/no-array-index-keyUsing array index as key prop

Hooks rules:

RuleWhat It Catches
react-hooks/rules-of-hooksHooks called conditionally or in loops
react-hooks/exhaustive-depsMissing dependencies in useEffect, useMemo, useCallback

TypeScript rules worth enabling:

RuleWhat It Catches
@typescript-eslint/no-unused-varsDeclared but unused variables
@typescript-eslint/no-explicit-anyUsing any type
@typescript-eslint/consistent-type-importsMissing type keyword on type-only imports
@typescript-eslint/no-non-null-assertionUsing ! non-null assertion
@typescript-eslint/prefer-nullish-coalescingUsing logical OR instead of ??

TypeScript Notes

// consistent-type-imports enforces this:
import type { User } from "@/types"; // type-only import
import { fetchUser } from "@/lib/api"; // value import
 
// Instead of mixing them:
import { User, fetchUser } from "@/lib/api"; // ❌ lint error

Gotchas

Things that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.

  • exhaustive-deps false positives — This rule sometimes flags stable references like dispatch or refs. Fix: Use // eslint-disable-next-line react-hooks/exhaustive-deps only when you are certain the dependency is stable. Never disable it globally.

  • no-unused-vars conflicts with TypeScript — The base ESLint no-unused-vars and @typescript-eslint/no-unused-vars can conflict. Fix: Turn off the base rule and only use the TypeScript version: "no-unused-vars": "off".

  • import/order not auto-fixing — The rule reports violations but the --fix only works for reordering, not adding newlines between groups retroactively. Fix: Run eslint --fix and manually add blank lines on the first pass.

  • Severity matters for CI — Using "warn" means CI passes even with violations. If you want to enforce a rule, use "error". Fix: Reserve "warn" for rules you are migrating toward, use "error" for enforced rules.

Alternatives

Other ways to solve the same problem — and when each is the better choice.

AlternativeUse WhenDon't Use When
next/core-web-vitals defaultsYou want sensible defaults without customizationYou need stricter or project-specific rules
Biome lint rulesYou want faster linting with built-in rulesYou need the full breadth of ESLint plugins
TypeScript compiler (tsc --noEmit)You want type-level checks that ESLint cannot doYou need code style or pattern enforcement

FAQs

What is the difference between "off", "warn", and "error" severity levels?
  • "off" (0) disables the rule completely.
  • "warn" (1) shows a yellow warning but does not fail CI or block builds.
  • "error" (2) shows a red error, fails CI, and blocks builds.
Why should I use @typescript-eslint/no-unused-vars instead of the base no-unused-vars?
  • The base ESLint no-unused-vars does not understand TypeScript syntax (interfaces, type aliases, enums).
  • The two rules conflict when both are enabled.
  • Turn off the base rule and use only the TypeScript version:
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
What does consistent-type-imports enforce?
// Enforced (correct):
import type { User } from "@/types";
import { fetchUser } from "@/lib/api";
 
// Rejected (lint error):
import { User, fetchUser } from "@/lib/api";

It separates type-only imports from value imports so bundlers can tree-shake types.

How does the import/order rule organize imports?
  • Groups imports into categories: builtin, external, internal, parent/sibling, index, type.
  • Enforces blank lines between groups with "newlines-between": "always".
  • Alphabetizes imports within each group.
Gotcha: Why does import/order not fully auto-fix on the first run?
  • eslint --fix can reorder imports within groups.
  • It does not always add missing blank lines between groups retroactively.
  • Run eslint --fix once, then manually add blank lines on the initial pass.
When should I use "warn" versus "error" for a rule?
  • Use "warn" for rules you are migrating toward or that are advisory.
  • Use "error" for rules you want to enforce strictly in CI.
  • Remember: "warn" will not fail CI, so violations accumulate silently if you forget to promote to "error".
What does react-hooks/exhaustive-deps catch?
  • Missing dependencies in useEffect, useMemo, and useCallback dependency arrays.
  • Adding all dependencies prevents stale closure bugs.
  • Set to "warn" because it can produce false positives with stable references like dispatch.
Gotcha: Can I safely disable exhaustive-deps globally?
  • No. Disabling it globally hides real bugs where effects use stale values.
  • Instead, suppress it per-line with // eslint-disable-next-line react-hooks/exhaustive-deps only when you are certain the dependency is stable.
What does react/jsx-no-target-blank protect against?
  • Links with target="_blank" without rel="noreferrer" expose your page to window.opener attacks.
  • The rule enforces adding rel="noreferrer" to all external links.
How do I pass options to a rule?
// Severity only:
"rule-name": "error"
 
// Severity with options:
"rule-name": ["error", { option: "value" }]

The second element of the array is the rule's options object.

What TypeScript-specific rules should I enable beyond the defaults?
RulePurpose
@typescript-eslint/no-explicit-anyFlags any usage
@typescript-eslint/no-non-null-assertionFlags ! assertions
@typescript-eslint/prefer-nullish-coalescingPrefers ?? over ||
@typescript-eslint/consistent-type-importsEnforces import type
How do I override a rule set by next/core-web-vitals?
  • Add a rules object after the extends array in your flat config.
  • Your rules override the preset's defaults.
const eslintConfig = [
  ...compat.extends("next/core-web-vitals"),
  { rules: { "react/no-unescaped-entities": "off" } },
];