Whether you're a senior developer or just starting out, writing clean, consistent code isn't optional — it's the baseline. The good news is you don't have to enforce it by hand. In this guide, we'll wire up Prettier, ESLint, Husky, and lint-staged so your codebase enforces its own standards automatically, on every single commit.
What we're building
| Tool | Job |
|---|---|
| ESLint | Catches bugs and enforces code-quality rules |
| Prettier | Formats code consistently — no more style debates |
| Husky | Runs scripts automatically at git hook points (e.g. pre-commit) |
| lint-staged | Runs linters/formatters only on the files you've staged, not the whole repo |
By the end, every commit will be automatically linted and formatted — and bad commits won't get through.
Prerequisites
- [ ] Node.js 18.18+ (Node 20+ recommended)
- [ ] A React project already set up with Vite + ESLint
- [ ] A git repository (
git initif you haven't already — Husky needs this) We'll use Husky v9's simplified init flow, which is the current standard as of 2026.
In this guide
- Scaffold the project
- Install and configure Prettier
- Add a formatting script
- Stop ESLint and Prettier from conflicting
- Install and initialize Husky
- Add lint-staged
- Test the full setup
- (Optional) VS Code integration
- (Optional) Enforce commit message format
1. Scaffold the project
Start a new project using Vite, choosing React with ESLint and TypeScript (recommended):
yarn create vite
Just follow the on-screen prompts — that's all it takes.
2. Install and configure Prettier
yarn add -D prettier
Create a .prettierrc in your project root:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"arrowParens": "always"
}
💡 Adjust these to your team's taste — they're just the most commonly used defaults.
Then create a .prettierignore next to it:
dist
build
node_modules
coverage
package-lock.json
pnpm-lock.yaml
yarn.lock
3. Add a formatting script
Your package.json likely already has a lint script from the Vite template. Add formatting scripts alongside it:
{
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
Test it:
yarn format:check
4. Stop ESLint and Prettier from conflicting
Some ESLint stylistic rules (spacing, semicolons, quotes) can disagree with Prettier's output. Rather than fighting rule-by-rule, install eslint-config-prettier, which simply turns off any ESLint rule that would conflict with Prettier:
yarn add -D eslint-config-prettier
Then wire it into your eslint.config.js:
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
import eslintConfigPrettier from 'eslint-config-prettier';
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
eslintConfigPrettier, // 👈 must come last
],
languageOptions: {
globals: globals.browser,
},
},
]);
5. Install and initialize Husky
yarn add -D husky
yarn husky init
husky initdoes two things automatically:
- Creates a
.husky/directory with a sample pre-commit hook- Adds a
preparescript topackage.json:{ "scripts": { "prepare": "husky" } }
The prepare script runs automatically after npm install, so every teammate who clones the repo gets the git hooks set up — no manual step needed on their end.
ℹ️ Husky v9 hook files are plain shell scripts with no shebang line needed. (Older guides showing
#!/usr/bin/env shat the top are for Husky v8 and earlier.)
6. Add lint-staged
Running ESLint and Prettier across the entire project on every commit gets slow as the codebase grows. lint-staged runs them only on the files you've actually staged.
yarn add -D lint-staged
Add a lint-staged block to package.json:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md,html}": [
"prettier --write"
]
}
}
Now replace the contents of .husky/pre-commit with:
npx lint-staged
That's it — no shebang, no sourcing a helper script.
7. Test the full setup
Intentionally mess up a file's formatting (extra spaces, wrong quotes, a missing semicolon), then stage and commit it:
git add .
git commit -m "test: verify pre-commit hooks"
You should see lint-staged run, auto-fix the formatting, and re-stage the file before the commit completes. If there's a real lint error (not just a style issue), the commit will be blocked until you fix it.
⚠️ Emergency bypass (not recommended as a habit):
git commit -m "message" --no-verify
8. (Optional) VS Code integration
Install the ESLint and Prettier VS Code extensions, then add .vscode/settings.json:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
This formats on save and auto-fixes lintable issues as you work, so the pre-commit hook rarely has anything left to do.
9. (Optional) Enforce commit message format
If you also want conventional commit messages enforced:
yarn add -D @commitlint/cli @commitlint/config-conventional
Create commitlint.config.js:
export default { extends: ['@commitlint/config-conventional'] };
Create .husky/commit-msg:
npx --no -- commitlint --edit "$1"
Now a commit message like asdf fix stuff gets rejected in favor of something like fix: correct button alignment.
Wrapping up
With this setup, every commit is automatically formatted, linted, and (optionally) checked for a proper message — before it ever leaves your machine. No more "please run Prettier before pushing" comments in code review.
Recap:
- ✅ Prettier formats your code consistently
- ✅
eslint-config-prettierkeeps ESLint and Prettier from fighting - ✅ Husky runs hooks automatically on every clone, thanks to
prepare - ✅ lint-staged keeps checks fast by only touching staged files
- ✅ (Optional) VS Code auto-fixes on save
- ✅ (Optional) Commitlint enforces a readable commit history
If you found this useful, let me know in the comments — happy committing! 🚀
Top comments (0)