Skip to main content

ESLint & Prettier in a Docusaurus Blog

· 6 min read ·
Docux
Curious explorer, a bit of a mad experimenter, and a bit of a contributor.

Developer Development License: MIT AI 90%

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.

Two tools, two jobs

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 key props, 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

> user@machine: ~/yourproject

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.

📁docux-blog
eslint.config.mjs
.prettierrc.json
.prettierignore
package.json

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-eslint so type-only imports (like CSSProperties) aren't wrongly flagged as unused, and .tsx files actually parse.
  • ecmaVersion: "latest" so recent syntax such as import attributes (import x from "./data.json" with { type: "json" }) is understood.
  • react/no-unescaped-entities is off. In a blog, JSX prose is full of apostrophes and quotes; this rule would drown the report in noise.
  • no-empty allows empty catch. Swallowing an error on purpose (analytics must never break navigation) is a legitimate pattern.
  • no-unused-vars with ignoreRestSiblings. This lets you destructure a key out only to drop it from a ...rest object without a false positive.
  • prettier is 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:

A React hook called conditionally

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 key prop on a <PostCard> inside a .map() — the classic React warning.
  • A comment rendered as visible text. A // note written directly in JSX children was being printed on the page (with a typo, classNamme, that nobody had noticed).
  • A documented-but-ignored prop. My Hero component declared a className prop 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.

note

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:

  1. A safety net — silly bugs are caught before they reach production.
  2. Consistency — the codebase stays readable and uniform, even months later.
  3. 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 lint or format:check fails.
  • 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.

Takeaway

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

No related posts.

Back to top