ESLint & Prettier in a Docusaurus Blog
For a long time this blog had no linter and no formatter. Files drifted into different styles (some indented with 4 spaces and single quotes, others with 2 spaces and double quotes), and nothing was watching for the small mistakes that slip into any React codebase. So I finally added ESLint and Prettier — and to my surprise, the linter immediately found real bugs that had been hiding in plain sight.
This article explains what these two tools actually do, how I configured them for a modern Docusaurus (React 19, a few TypeScript components, custom plugins), and — the fun part — the concrete issues ESLint caught in my own code.
Prettier handles form (how the code looks). ESLint handles substance (whether the code is correct). They complement each other; they don't compete.
ESLint vs Prettier: who does what?
It's easy to install both without knowing where one stops and the other begins.
- Prettier — the formatter. It only cares about layout: indentation, quotes, semicolons, line length, line breaks. It never judges your logic; it rewrites your code into one consistent style. You stop thinking about formatting: you type, you save, it's aligned.
- ESLint — the problem detector. It analyses the logic and flags what's suspicious or broken: unused variables, React hooks called conditionally, missing
keyprops, and so on.
To avoid the two stepping on each other, eslint-config-prettier turns off every ESLint rule that is purely about style, leaving formatting entirely to Prettier.
Installing the tooling
npm install -D eslint @eslint/js globals eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier prettier typescript typescript-eslint
I use flat config (eslint.config.mjs), the format ESLint recommends since v9. typescript + typescript-eslint are only needed because a few of my components are written in .tsx.
Prettier configuration
A tiny config file is enough. I keep Prettier's defaults and only pin a couple of choices:
{
"singleQuote": false,
"tabWidth": 2,
"semi": true,
"trailingComma": "es5",
"printWidth": 80,
"endOfLine": "auto"
}
The endOfLine: "auto" line matters on Windows: it stops format:check from failing when Git checks files out with CRLF endings while Prettier expects LF.
And a .prettierignore so generated and vendored files are left alone:
build/
.docusaurus/
node_modules/
static/
package-lock.json
*.min.js
ESLint flat configuration
Here is the whole config. A few decisions are worth calling out below it:
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
import prettier from "eslint-config-prettier";
// Shared rules for unused code, applied to both JS and TS blocks below.
const unusedVarsRule = [
"error",
{
// Ignore intentionally-unused args/vars prefixed with `_`.
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
// Allow destructuring a key out only to omit it from a `...rest` object
// (e.g. dropping `height`/`width` in MDXComponents).
ignoreRestSiblings: true,
},
];
export default tseslint.config(
{
// Generated output and vendored files are never linted.
ignores: ["build/**", ".docusaurus/**", "node_modules/**", "static/**"],
},
js.configs.recommended,
{
files: ["**/*.{js,jsx,mjs,cjs}"],
languageOptions: {
ecmaVersion: "latest", // Enables recent syntax such as import attributes.
sourceType: "module",
parserOptions: {
ecmaFeatures: { jsx: true },
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
react,
"react-hooks": reactHooks,
},
settings: {
react: { version: "detect" },
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
// Docusaurus injects React automatically and we rely on PropTypes only
// where it makes sense, so these two rules add noise here.
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
// Literal quotes/apostrophes in JSX prose are fine and would otherwise
// flood the report; keep the linter focused on real issues.
"react/no-unescaped-entities": "off",
// Empty `catch {}` is a deliberate "swallow" pattern for analytics/optional code.
"no-empty": ["error", { allowEmptyCatch: true }],
"no-unused-vars": unusedVarsRule,
},
},
{
// TypeScript / TSX components (parsed with the TS parser, no type-checking).
files: ["**/*.{ts,tsx}"],
extends: [tseslint.configs.recommended],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
react,
"react-hooks": reactHooks,
},
settings: {
react: { version: "detect" },
},
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/no-unescaped-entities": "off",
"no-empty": ["error", { allowEmptyCatch: true }],
// Use the TS-aware version so type-only imports aren't flagged as unused.
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": unusedVarsRule,
},
},
// Must stay last so it can turn off rules that conflict with Prettier.
prettier
);
The choices that make this pleasant to live with:
- Two blocks, one for JS/JSX and one for TS/TSX. The TypeScript block uses
typescript-eslintso type-only imports (likeCSSProperties) aren't wrongly flagged as unused, and.tsxfiles actually parse. ecmaVersion: "latest"so recent syntax such as import attributes (import x from "./data.json" with { type: "json" }) is understood.react/no-unescaped-entitiesis off. In a blog, JSX prose is full of apostrophes and quotes; this rule would drown the report in noise.no-emptyallows emptycatch. Swallowing an error on purpose (analytics must never break navigation) is a legitimate pattern.no-unused-varswithignoreRestSiblings. This lets you destructure a key out only to drop it from a...restobject without a false positive.prettieris last, so it can disable any stylistic rule that would conflict with the formatter.
The npm scripts
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
npm run lint— report problems.npm run lint:fix— auto-fix what it can.npm run format— reformat everything.npm run format:check— verify without touching files (great for CI).
What ESLint actually caught
This is the part that convinced me the effort was worth it. On a codebase that "worked fine", the linter surfaced 45 findings. Most were harmless cleanup, but a few were genuine bugs:
My BlueSkyShare component returned early before calling useDocusaurusContext():
export default function BlueSkyShare({ metadata }) {
const key = metadata?.frontMatter?.blueSkyRecordKey;
if (key) return; // ⛔ early return...
const { siteConfig } = useDocusaurusContext(); // ...before this hook
// ...
}
Hooks must run in the same order on every render — this can crash React. The fix is to call the hook first, unconditionally, and return null afterwards.
Other real issues:
- A missing
keyprop on a<PostCard>inside a.map()— the classic React warning. - A comment rendered as visible text. A
// notewritten directly in JSX children was being printed on the page (with a typo,classNamme, that nobody had noticed). - A documented-but-ignored prop. My
Herocomponent declared aclassNameprop in its PropTypes and JSDoc but never applied it. ESLint flagged it as unused; the real fix was to actually use it. - ~30 dead imports and variables (
clsx,CSSProperties, leftover helpers) removed across the project.
Handling react-hooks/exhaustive-deps carefully
The trickiest warnings are the exhaustive-deps ones. Do not blindly add every suggested dependency — it can cause infinite re-render or re-fetch loops.
When an effect is intentionally keyed on a subset of values, the honest fix is an explicit, justified disable comment:
useEffect(() => {
// ...fetch based on `username`
// Keyed on `username` only: adding `fetchAllRepos` (re-created each render)
// would re-trigger the fetch on every render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username]);
The point isn't to silence the linter — it's to document a deliberate decision so the next reader (often future you) understands why.
Where a missing dependency is genuinely stable (a value coming from config, for example), just add it. Reach for eslint-disable only when adding the dependency would change behavior.
The payoff
After triaging all 45 findings, npm run lint reports zero problems and npm run format:check passes cleanly. Concretely, this bought me:
- A safety net — silly bugs are caught before they reach production.
- Consistency — the codebase stays readable and uniform, even months later.
- Smoother contributions — anyone helping on GitHub is held to the same standard automatically.
Going further
The natural next step is to make these checks run automatically:
- In CI (GitHub Actions): fail the build if
lintorformat:checkfails. - On commit (with Husky + lint-staged): format and lint only the staged files before each commit.
That way the tooling protects the project without anyone having to remember to run it — a topic for a future article.
Prettier keeps your code pretty; ESLint keeps it correct. Adding both to a Docusaurus blog takes twenty minutes and pays for itself the moment the linter finds its first real bug.
This article is part of the Tools series:
- ESLint & Prettier in a Docusaurus Blog
Loading comments…
No related posts.
