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) andwarn(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-vitalspreset 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:
| Rule | What It Catches |
|---|---|
react/jsx-no-target-blank | Missing rel="noreferrer" on target="_blank" links |
react/no-unescaped-entities | Unescaped ' or " in JSX text |
react/self-closing-comp | <div></div> instead of <div /> for empty elements |
react/jsx-curly-brace-presence | Unnecessary {"string"} instead of string in JSX |
react/no-array-index-key | Using array index as key prop |
Hooks rules:
| Rule | What It Catches |
|---|---|
react-hooks/rules-of-hooks | Hooks called conditionally or in loops |
react-hooks/exhaustive-deps | Missing dependencies in useEffect, useMemo, useCallback |
TypeScript rules worth enabling:
| Rule | What It Catches |
|---|---|
@typescript-eslint/no-unused-vars | Declared but unused variables |
@typescript-eslint/no-explicit-any | Using any type |
@typescript-eslint/consistent-type-imports | Missing type keyword on type-only imports |
@typescript-eslint/no-non-null-assertion | Using ! non-null assertion |
@typescript-eslint/prefer-nullish-coalescing | Using 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 errorGotchas
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
dispatchor refs. Fix: Use// eslint-disable-next-line react-hooks/exhaustive-depsonly when you are certain the dependency is stable. Never disable it globally. -
no-unused-vars conflicts with TypeScript — The base ESLint
no-unused-varsand@typescript-eslint/no-unused-varscan 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
--fixonly works for reordering, not adding newlines between groups retroactively. Fix: Runeslint --fixand 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.
| Alternative | Use When | Don't Use When |
|---|---|---|
next/core-web-vitals defaults | You want sensible defaults without customization | You need stricter or project-specific rules |
| Biome lint rules | You want faster linting with built-in rules | You need the full breadth of ESLint plugins |
TypeScript compiler (tsc --noEmit) | You want type-level checks that ESLint cannot do | You 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-varsdoes 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 --fixcan reorder imports within groups.- It does not always add missing blank lines between groups retroactively.
- Run
eslint --fixonce, 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, anduseCallbackdependency arrays. - Adding all dependencies prevents stale closure bugs.
- Set to
"warn"because it can produce false positives with stable references likedispatch.
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-depsonly when you are certain the dependency is stable.
What does react/jsx-no-target-blank protect against?
- Links with
target="_blank"withoutrel="noreferrer"expose your page towindow.openerattacks. - 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?
| Rule | Purpose |
|---|---|
@typescript-eslint/no-explicit-any | Flags any usage |
@typescript-eslint/no-non-null-assertion | Flags ! assertions |
@typescript-eslint/prefer-nullish-coalescing | Prefers ?? over || |
@typescript-eslint/consistent-type-imports | Enforces import type |
How do I override a rule set by next/core-web-vitals?
- Add a
rulesobject after theextendsarray 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" } },
];Related
- ESLint Setup for Next.js — initial ESLint configuration
- ESLint Plugins — install and configure plugins
- TypeScript Strict Mode — complement ESLint with strict compiler options
- ESLint + Prettier Integration — avoid formatting conflicts