ESLint & Prettier
Linting and formatting are two distinct jobs that work best together. ESLint analyzes your code for correctness problems—a forgotten dependency in useEffect, a missing key, an unused variable—while Prettier handles style by reprinting your code in one canonical shape. Splitting the responsibilities keeps each tool focused: ESLint catches bugs, Prettier ends debates about quotes and commas. This page covers a modern flat ESLint config for React, the indispensable react-hooks and jsx-a11y plugins, wiring Prettier in cleanly, editor integration, and a pre-commit hook so nothing slips through.
ESLint with the flat config
Since ESLint 9, the default is “flat config”—a single eslint.config.js that exports an array of config objects instead of the older .eslintrc cascade. Each object can target specific files and layer on plugins and rules. For a React project you want the recommended JavaScript rules, the React plugin, the Hooks plugin, and accessibility checks.
npm install -D eslint @eslint/js eslint-plugin-react-hooks \
eslint-plugin-jsx-a11y globals
// eslint.config.js
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
export default [
{ ignores: ["dist", "build"] },
js.configs.recommended,
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: {
"react-hooks": reactHooks,
"jsx-a11y": jsxA11y,
},
rules: {
...reactHooks.configs.recommended.rules,
...jsxA11y.flatConfigs.recommended.rules,
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
},
},
];
Run it from an npm script so the whole team uses the same invocation:
npx eslint .
Output:
/src/components/SearchBox.jsx
14:6 warning React Hook useEffect has a missing dependency: 'query'.
Either include it or remove the dependency array
react-hooks/exhaustive-deps
✖ 1 problem (0 errors, 1 warning)
The hooks rules are non-negotiable
eslint-plugin-react-hooks enforces the two Rules of Hooks that the React runtime depends on. rules-of-hooks flags calling a hook conditionally or inside a loop, which would corrupt React’s internal hook order. exhaustive-deps checks that every reactive value a hook reads appears in its dependency array—the single most common source of stale-closure bugs.
function Search({ query }) {
const [results, setResults] = useState([]);
// Without `query` in the deps, the effect captures the first
// value forever. exhaustive-deps catches this at lint time.
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then((r) => r.json())
.then(setResults);
}, [query]);
return <ResultList items={results} />;
}
Resist disabling
exhaustive-depswith a comment. The warning almost always points at a real bug; the right fix is usually to add the dependency, move the value out, or wrap a function inuseCallback.
Accessibility checks with jsx-a11y
eslint-plugin-jsx-a11y statically analyzes your JSX for common accessibility mistakes—images without alt, click handlers on non-interactive elements, redundant ARIA roles, or inputs with no associated label. It cannot catch everything a screen-reader audit would, but it eliminates entire classes of regressions for free, every time you save.
// Flags: jsx-a11y/alt-text — img elements must have an alt prop.
<img src="/logo.svg" />
// Correct:
<img src="/logo.svg" alt="DevCraftly logo" />
Prettier, without the conflicts
Prettier is opinionated by design: you configure a few preferences and it formats everything else consistently. Install it and define a small .prettierrc.
npm install -D prettier
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 80
}
The classic pitfall is ESLint and Prettier fighting over stylistic rules. The modern flat-config approach simply doesn’t enable formatting rules in ESLint—the config above doesn’t, so there’s nothing to conflict with. Keep linting and formatting as separate commands rather than running Prettier through ESLint.
| Tool | Responsibility | Example rule |
|---|---|---|
| ESLint | Code correctness & patterns | react-hooks/exhaustive-deps |
| Prettier | Whitespace & punctuation | printWidth, trailingComma |
| jsx-a11y | Accessibility in JSX | jsx-a11y/alt-text |
Wire both into package.json so they’re easy to run and compose in CI:
{
"scripts": {
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
Editor integration
The real payoff comes from running these tools as you type. In VS Code, install the ESLint and Prettier extensions, then format on save and let ESLint auto-fix what it can.
// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
Commit this file so every contributor gets the same in-editor behavior with zero setup.
Pre-commit hooks
CI catching a lint error after you push is slow feedback. A pre-commit hook runs the checks on staged files before the commit lands. husky installs the Git hook and lint-staged runs the right tool against only the files you changed.
npm install -D husky lint-staged
npx husky init
// package.json
{
"lint-staged": {
"*.{js,jsx}": ["eslint --fix", "prettier --write"],
"*.{json,css,md}": ["prettier --write"]
}
}
# .husky/pre-commit
npx lint-staged
Now a commit auto-formats touched files and aborts if ESLint finds an unfixable error—the fast, local feedback loop that keeps main clean.
Best Practices
- Use the flat
eslint.config.js; enablereact-hooksandjsx-a11yrecommended rules as a baseline. - Keep ESLint for correctness and Prettier for style—don’t run formatting rules through ESLint, and you’ll never fight conflicts.
- Treat
exhaustive-depswarnings as bugs; fix the dependencies instead of silencing the rule. - Commit
.vscode/settings.jsonand.prettierrcso the whole team formats identically. - Run
prettier --checkandeslint .in CI as a backstop behind the pre-commit hook. - Use
lint-stagedso hooks stay fast by only touching changed files. - Pin tool versions in
devDependenciesto avoid surprise rule changes across machines.