Module 8: Tooling and Testing
Linting, Formatting and Build Tooling
What ESLint and Prettier each do, the Vite config options you'll actually use, path aliases, environment files, and keeping a team's code consistent.
None of this is glamorous. All of it pays for itself within a week on any project with more than one person, and within a month on a project with just you. A good setup means fewer review comments about spacing, fewer bugs from forgotten dependencies, and a code base that looks like one person wrote it.
ESLint: finding bugs#
ESLint reads your code and reports problems. Not style preferences, but things like unused variables, missing hook dependencies, and unreachable code.
The Vite React template already ships with it configured. Look at eslint.config.js (the "flat config" format):
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{js,jsx}"],
extends: [js.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
languageOptions: {
ecmaVersion: "latest",
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
},
},
]);
Your template's exact contents depend on when you scaffolded it, so don't worry if yours looks slightly different. The important pieces:
react-hooks: enforces the rules of hooks and checks dependency arrays. It's the single most valuable rule set for React code.react-refresh: warns when a file exports things in a way that breaks fast refresh (for instance, exporting both a component and a non-component from one file).js.configs.recommended: the standard set of "this is probably a mistake" rules.
Run it with npm run lint. Your editor can run it as you type if you install its ESLint extension.
Prettier: ending formatting arguments#
Prettier rewrites your code into a consistent style: quotes, line length, trailing commas, indentation. Nobody chooses these, the tool does. The value isn't the style itself, it's that style debates stop.
npm install -D prettier eslint-config-prettier
// .prettierrc
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100
}
// package.json scripts
"format": "prettier --write .",
"format:check": "prettier --check ."
eslint-config-prettier turns off any ESLint rules that would conflict with Prettier's formatting, so the two don't fight. Add it last in your ESLint config's extends.
The division of labor to remember: ESLint finds problems, Prettier handles appearance. Turn on "format on save" in your editor and you'll never think about it again.
Pre-commit hooks#
People forget to run the linter. A git hook can do it for them, only on the files you're committing.
npm install -D husky lint-staged
npx husky init
// package.json
"lint-staged": {
"*.{js,jsx}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": ["prettier --write"]
}
# .husky/pre-commit
npx lint-staged
Now a commit with a lint error is rejected before it leaves your machine. On a team, also run lint, tests and build in CI (GitHub Actions, for example) on every pull request. The hook is convenience, CI is enforcement.
vite.config.js worth knowing#
The config file is small. These are the options you'll touch.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:8080", // forward API calls in dev, avoids CORS
changeOrigin: true,
},
},
},
build: {
sourcemap: true,
},
});
Path aliases turn import Button from "../../../components/Button" into import Button from "@/components/Button". Deep relative imports get brittle fast, since moving a file breaks every path. To make your editor understand the alias, add a jsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}
The dev proxy is a lifesaver. Your frontend runs at localhost:5173, your backend at localhost:8080, and the browser blocks cross-origin requests unless the server allows it. With the proxy, the browser calls /api/... on the same origin, and Vite forwards it. No CORS config needed during development.
Environment files#
Vite loads different .env files by mode:
.env always
.env.local always, git-ignored (your personal overrides)
.env.development npm run dev
.env.production npm run build
Only variables starting with VITE_ reach your code. Commit an .env.example listing the names without values, so new teammates know what to set.
Scripts you want in package.json#
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest",
"test:run": "vitest run"
}
A good habit before opening a pull request: npm run lint && npm run test:run && npm run build. If all three pass locally, CI will almost certainly agree.
TypeScript#
We've used plain JavaScript in this course, but most professional React codebases today use TypeScript. It catches wrong prop types, misspelled fields and null-handling mistakes before the code runs. When you're comfortable with the basics here, scaffold a project with the react-ts template and re-do an earlier exercise. React's types for props, events and hooks are the main thing to learn. The concepts you've learned carry over unchanged.
Where people get burned#
- Disabling lint rules with comments rather than fixing the issue.
- Running Prettier on some machines and not others, producing noisy diffs. Commit the config and use a hook.
- Letting ESLint and Prettier conflict, so each undoes the other.
- Committing
.env.localor a file with real secrets. - Deep
../../..imports everywhere. - Hitting CORS errors in dev and "fixing" them by turning off web security in the browser, instead of using the proxy.
- Having no CI, so broken builds reach the main branch.
Practice#
- Run
npm run linton a project and fix every warning without disabling any rules. - Install Prettier, add the config, and format the project. Turn on format-on-save in your editor.
- Set up Husky and lint-staged, then try to commit a file with an unused variable and watch the commit fail.
- Add a
@alias forsrc, convert three imports to use it, and confirm both the build and your editor understand it. - Add a dev proxy for
/apiand make a request to it from a component.