Introduction
Welcome back, future React pro! In the journey of building robust and scalable React applications, writing functional code is just one piece of the puzzle. Equally important is writing clean, consistent, and maintainable code. This is where linting, formatting, and pre-commit hooks come into play.
In this chapter, we’re going to dive deep into these essential tools that elevate your code quality from good to great. You’ll learn how to set up powerful linters to catch potential errors and enforce best practices, integrate formatters to keep your code looking sharp and consistent, and automate these checks using pre-commit hooks. By the end, your codebase will not only work flawlessly but also be a joy to read and collaborate on, making you a more effective and professional developer.
This chapter builds upon your existing knowledge of setting up a React project and managing dependencies. We’ll be working with configuration files and command-line tools, so get ready to add some powerful automation to your development workflow!
Core Concepts: Ensuring Code Quality and Consistency
Let’s start by understanding what linting, formatting, and pre-commit hooks are, and why they’re indispensable for any modern React project.
What are Linting and Formatting?
While often used together, linting and formatting serve distinct purposes:
- Linting (with ESLint): Think of a linter as a strict code critic. It analyzes your code for potential errors, suspicious constructs, deviations from coding standards, and even stylistic issues that could lead to bugs or make the code harder to understand. For instance, it can warn you about unused variables, unreachable code, or incorrect dependency arrays in React Hooks. It’s about code correctness and best practices.
- Formatting (with Prettier): A formatter, on the other hand, is like a meticulous housekeeper for your code. It automatically restructures your code according to a predefined set of style rules (e.g., indentation, line length, semicolon usage, bracket placement). It doesn’t care about logical errors; its sole purpose is to make your code look consistent and tidy. It’s about code aesthetics.
Why are they important? Imagine working on a team where everyone uses a different coding style. Some use tabs, others spaces. Some put semicolons, others don’t. Some prefer single quotes, others double. This quickly becomes a nightmare for readability and merge conflicts.
- Consistency: Both linting and formatting enforce a consistent code style across your entire project and team. This makes the codebase easier to read, understand, and navigate for everyone.
- Error Prevention: Linters catch common programming errors and anti-patterns before you even run your code, saving you valuable debugging time.
- Maintainability: Consistent and error-free code is inherently more maintainable and less prone to introducing new bugs during future modifications.
- Collaboration: When everyone adheres to the same standards, code reviews become focused on logic and architecture, not stylistic nitpicks.
Introducing ESLint: Your Code Quality Guardian
ESLint is the de-facto standard for linting JavaScript and TypeScript code. It’s highly configurable, allowing you to define a vast array of rules and even extend existing configurations from popular style guides (like Airbnb or React’s recommended rules).
How does ESLint work? ESLint takes your code, parses it into an Abstract Syntax Tree (AST), and then runs various rules against this AST to identify issues. When it finds something problematic, it reports it, often with suggestions on how to fix it.
Introducing Prettier: The Uncompromising Code Formatter
Prettier is an opinionated code formatter that enforces a consistent style by parsing your code and re-printing it with its own rules. The beauty of Prettier is its “opinionated” nature – it has very few configuration options, meaning less time spent debating style guides and more time coding!
How does Prettier work? You feed Prettier your messy, inconsistently formatted code, and it spits out beautifully formatted code, automatically handling things like indentation, line breaks, and spacing. It supports many languages, including JavaScript, TypeScript, JSX, CSS, HTML, and more.
Automating Checks with Pre-commit Hooks (Husky & lint-staged)
Manually running your linter and formatter before every commit is tedious and easy to forget. That’s where pre-commit hooks come in.
A Git hook is a script that Git executes automatically before or after events like commit, push, and receive. A pre-commit hook runs before a commit is finalized. If the script exits with a non-zero status (indicating failure), Git aborts the commit.
- Husky: Husky is a tool that makes it easy to manage Git hooks. Instead of manually messing with the
.git/hooksdirectory, you configure your hooks inpackage.json, and Husky takes care of linking them. - lint-staged: While Husky runs scripts for every commit,
lint-stagedis a companion tool that lets you run linters and formatters only on the files staged for commit. This is a huge performance booster, as you don’t need to check your entire codebase every time you commit a small change.
The Workflow:
This workflow ensures that only code that passes your defined quality and style checks ever makes it into your repository, keeping your main branch pristine.
Step-by-Step Implementation: Setting Up Our Tools
Let’s get our hands dirty and integrate these tools into a new or existing React project. We’ll assume you have a basic React project set up (e.g., created with Vite or Create React App, though Vite is generally preferred for modern React development).
Step 1: Initialize Your Project (if you haven’t already)
If you’re starting fresh, let’s quickly create a Vite + React project. We’ll use Node.js v22.x (LTS as of early 2026) and npm for package management.
# Ensure you have Node.js v22.x installed
node --version
npm --version
# Create a new Vite React project
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
# Install dependencies
npm install
Great! Now you have a clean React TypeScript project ready for our tools.
Step 2: Setting up ESLint for React and TypeScript
First, we need to install ESLint and its related plugins for React and TypeScript.
Install ESLint and Plugins: Open your terminal in the
my-react-appdirectory and install the necessary packages. We’ll target modern versions as of 2026-01-31.npm install --save-dev [email protected] @typescript-eslint/[email protected] @typescript-eslint/[email protected] [email protected] [email protected] [email protected][email protected]: The core ESLint library.@typescript-eslint/[email protected]: ESLint rules specific to TypeScript.@typescript-eslint/[email protected]: A parser that allows ESLint to understand TypeScript syntax.[email protected]: ESLint rules for React.[email protected]: Specific rules for enforcing the Rules of Hooks.[email protected]: Rules for accessibility (a11y) best practices in JSX.
Create ESLint Configuration File: Create a file named
.eslintrc.cjs(using.cjsfor CommonJS configuration in a potentially ESM project, which is a modern best practice for Node.js config files) in the root of your project.// .eslintrc.cjs module.exports = { root: true, // Stops ESLint from looking for config files in parent folders env: { browser: true, // Enables browser global variables es2020: true, // Enables ES2020 global variables and parsing }, extends: [ 'eslint:recommended', // ESLint's recommended rules 'plugin:@typescript-eslint/recommended', // TypeScript recommended rules 'plugin:react/recommended', // React recommended rules 'plugin:react-hooks/recommended', // React Hooks recommended rules 'plugin:jsx-a11y/recommended', // Accessibility rules for JSX ], parser: '@typescript-eslint/parser', // Specifies the ESLint parser for TypeScript parserOptions: { ecmaVersion: 'latest', // Allows for the parsing of modern ECMAScript features sourceType: 'module', // Allows for the use of imports ecmaFeatures: { jsx: true, // Enables JSX }, project: './tsconfig.json', // Path to your tsconfig.json for type-aware linting }, settings: { react: { version: 'detect', // Tells eslint-plugin-react to automatically detect the React version }, 'import/resolver': { // Optional: For import linting if you add eslint-plugin-import later typescript: true, node: true, }, }, plugins: [ 'react', 'react-hooks', '@typescript-eslint', 'jsx-a11y', ], rules: { // Add or override specific rules here 'react/react-in-jsx-scope': 'off', // Not needed with React 17+ JSX transform 'react/prop-types': 'off', // Not needed when using TypeScript for prop validation '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], // Warn on unused, allow underscore prefix // Example: Enforce specific naming conventions '@typescript-eslint/naming-convention': [ 'error', { selector: 'variable', format: ['camelCase', 'UPPER_CASE', 'PascalCase'], leadingUnderscore: 'allow', }, { selector: 'function', format: ['camelCase', 'PascalCase'], }, { selector: 'typeLike', format: ['PascalCase'], }, ], // You can add more custom rules or override existing ones // 'no-console': 'warn', // Example: Warn about console.log in production }, ignorePatterns: ['dist', '.eslintrc.cjs'], // Files/folders to ignore from linting };Explanation:
root: true: Prevents ESLint from looking for configuration files in parent directories, ensuring your project’s configuration is self-contained.env: Specifies the environments your code runs in, providing access to global variables (e.g.,windowinbrowser,Promiseines2020).extends: An array of configurations to extend. We’re using recommended rules from ESLint itself, TypeScript, React, React Hooks, and JSX A11y. This gives us a solid baseline without configuring every rule manually.parser: Tells ESLint to use the TypeScript parser.parserOptions: Configures the parser, enabling modern JavaScript features and JSX.projectis crucial for type-aware linting.settings: Provides settings for plugins, like detecting the React version.plugins: Explicitly lists the plugins being used.rules: This is where you can customize rules. We’ve turned offreact/react-in-jsx-scope(modern React doesn’t requireimport React from 'react'for JSX) andreact/prop-types(TypeScript handles prop validation). We also added a helpful rule for unused variables and a modern naming convention.ignorePatterns: Specifies files and directories that ESLint should ignore.
Add ESLint Script to
package.json: Open yourpackage.jsonand add a new script under the"scripts"section:// package.json { "name": "my-react-app", // ... other fields "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, // ... other fields }"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0": This script runs ESLint on all.tsand.tsxfiles in your project, reports any unusedeslint-disabledirectives, and exits with an error if there are any warnings.
Test ESLint: Now, run your lint script:
npm run lintInitially, you might see some warnings or errors, especially in the default
App.tsxormain.tsxfiles. For example, Vite’s defaultsrc/App.tsxmight havecountandsetCountvariables that ESLint flags if not used.Let’s make a deliberate error in
src/App.tsxto see ESLint in action. ChangeApp.tsxto:// src/App.tsx import { useState } from 'react'; import reactLogo from './assets/react.svg'; import viteLogo from '/vite.svg'; import './App.css'; function App() { const [count, setCount] = useState(0); const unusedVariable = 'I am not used!'; // Deliberate unused variable const handleClick = () => { setCount((count) => count + 1); }; return ( <> <div> <a href="https://vitejs.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <div className="card"> <button onClick={handleClick}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ); } export default App;Run
npm run lintagain. You should now see an error or warning aboutunusedVariable! ESLint is working!
Step 3: Integrating Prettier for Automatic Formatting
Next up, let’s bring in Prettier to handle our code formatting.
Install Prettier and ESLint Integration: We need Prettier itself, plus two packages to ensure ESLint and Prettier play nicely together:
eslint-config-prettier(turns off ESLint rules that conflict with Prettier) andeslint-plugin-prettier(runs Prettier as an ESLint rule).npm install --save-dev [email protected] [email protected] [email protected][email protected]: The core Prettier library.[email protected]: Turns off all ESLint rules that are unnecessary or might conflict with Prettier. This must be the last entry in yourextendsarray in.eslintrc.cjs.[email protected]: Runs Prettier as an ESLint rule, reporting formatting issues as ESLint errors.
Update ESLint Configuration: Open
.eslintrc.cjsand add'plugin:prettier/recommended'to yourextendsarray. Remember, it must be the last entry to ensure it overrides any conflicting rules from previous extensions.// .eslintrc.cjs module.exports = { // ... (previous configuration) extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:prettier/recommended', // <--- THIS MUST BE THE LAST EXTENSION ], // ... (rest of the configuration) rules: { // ... (your existing rules) 'prettier/prettier': 'error', // Ensures Prettier formatting issues are reported as ESLint errors }, };Explanation:
'plugin:prettier/recommended'is a shortcut that does two things:- It adds
eslint-plugin-prettierto yourplugins. - It adds
eslint-config-prettierto yourextendsarray.
- It adds
'prettier/prettier': 'error'makes sure that any formatting violations detected by Prettier are reported as ESLint errors, failing the lint check.
Create Prettier Configuration File: Create a file named
.prettierrc.cjsin the root of your project. This is where you define your specific formatting preferences.// .prettierrc.cjs module.exports = { semi: true, // Add semicolons at the end of statements trailingComma: 'all', // Add trailing commas wherever possible (objects, arrays, functions) singleQuote: true, // Use single quotes instead of double quotes printWidth: 100, // Line length limit tabWidth: 2, // Number of spaces per indentation level useTabs: false, // Use spaces instead of tabs jsxSingleQuote: false, // Use double quotes for JSX attributes };Explanation: These are common settings. Feel free to adjust them to your team’s preferences. The beauty is that once defined, everyone gets the same formatting!
Add Prettier Script to
package.json(Optional but Recommended): You can add a script to format your entire project manually.// package.json { "name": "my-react-app", // ... other fields "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "preview": "vite preview" }, // ... other fields }"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"": This script tells Prettier to find and format (overwrite) files in thesrcdirectory (and other common file types) that match the glob pattern.
Test Prettier: Make some intentional formatting mistakes in
src/App.tsx. For example, use double quotes, remove a semicolon, or mess up indentation.// src/App.tsx (intentionally bad formatting) import { useState } from 'react' import reactLogo from './assets/react.svg'; import viteLogo from '/vite.svg' import './App.css'; function App() { const [count, setCount] = useState(0) const unusedVariable = "I am not used!" // Double quotes, no semicolon const handleClick = () => { setCount((count) => count + 1) } return ( <> <div> <a href="https://vitejs.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <div className="card"> <button onClick={handleClick}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ) } export default AppNow, run:
npm run formatYou should see Prettier automatically fix all the formatting issues! If you run
npm run lintnow, it should pass (after fixingunusedVariableor ignoring it with// eslint-disable-next-line @typescript-eslint/no-unused-vars).
Step 4: Configuring IDE Integration (VS Code Example)
While command-line tools are great, having your IDE automatically format and lint on save is a huge productivity boost. For VS Code, follow these steps:
Install Extensions:
- ESLint: Search for “ESLint” by Dirk Baeumer in the VS Code Extensions view and install it.
- Prettier - Code formatter: Search for “Prettier - Code formatter” by Esben Petersen and install it.
Configure VS Code Settings: Open your VS Code settings (File > Preferences > Settings or
Ctrl+,). Search for:Editor: Default Formatter: SelectPrettier - Code formatter.Editor: Format On Save: Check this box.Eslint: Validate: Ensurejavascript,typescript,javascriptreact,typescriptreactare included.
You can also create a
.vscode/settings.jsonfile in your project to enforce these settings for anyone opening the project:// .vscode/settings.json { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" // Automatically fix ESLint issues on save }, "eslint.validate": [ "javascript", "typescript", "javascriptreact", "typescriptreact" ] }Explanation:
editor.defaultFormatter: Sets Prettier as the default formatter.editor.formatOnSave: Enables automatic formatting when you save a file.editor.codeActionsOnSave: Tells VS Code to run ESLint’s auto-fix feature on save for all relevant file types. This is incredibly powerful for instantly resolving many linting issues.
Now, when you save a .tsx file, Prettier will format it, and ESLint will attempt to fix any auto-fixable errors!
Step 5: Setting up Pre-commit Hooks with Husky and lint-staged
Finally, let’s automate these checks to run before every commit.
Install Husky and lint-staged:
npm install --save-dev [email protected] [email protected][email protected]: The tool for managing Git hooks.[email protected]: Runs commands on staged Git files.
Initialize Husky: Husky needs to be initialized to set up the Git hooks directory.
npx husky initThis command creates a
.husky/directory in your project root and adds apre-commitsample hook.Configure the
pre-commitHook: Open the newly created.husky/pre-commitfile. Replace its content with the following:#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-stagedExplanation: This script simply tells Husky to run
npx lint-stagedbefore every commit.Configure
lint-stagedinpackage.json: Add a new section forlint-stagedin yourpackage.json. This tellslint-stagedwhat commands to run for which types of staged files.// package.json { "name": "my-react-app", // ... other fields "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "preview": "vite preview" }, "lint-staged": { "src/**/*.{ts,tsx,js,jsx}": [ "eslint --fix", // First, fix ESLint issues "prettier --write" // Then, format with Prettier ], "src/**/*.{json,css,md}": [ // For other file types, just run prettier "prettier --write" ] }, // ... other fields }Explanation:
"src/**/*.{ts,tsx,js,jsx}": This glob pattern matches all TypeScript and JavaScript files (including JSX) in yoursrcdirectory.["eslint --fix", "prettier --write"]: For these matched files,lint-stagedwill first runeslint --fix(which attempts to auto-fix linting issues) and thenprettier --write(to format them). The--fixand--writeflags are important for automatic corrections."src/**/*.{json,css,md}": For other file types, we only run Prettier.
Test the Pre-commit Hook: Let’s make some deliberate mistakes again in
src/App.tsx. Create an unused variable, mess up indentation, use double quotes, and remove a semicolon.// src/App.tsx (intentionally bad formatting and linting) import { useState } from 'react' // no semicolon import reactLogo from './assets/react.svg'; import viteLogo from '/vite.svg' // no semicolon import './App.css'; function App() { const [count, setCount] = useState(0) // no semicolon const anotherUnusedVariable = "I am also not used!" // double quotes, no semicolon const handleClick = () => { setCount((count) => count + 1) // no semicolon } return ( <> <div> <a href="https://vitejs.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <div className="card"> <button onClick={handleClick}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ) } export default AppNow, stage the changes and try to commit:
git add . git commit -m "Testing pre-commit hooks"What should happen:
- Husky will trigger the
pre-commithook. lint-stagedwill run ESLint and Prettier onsrc/App.tsx.- Prettier will automatically fix all formatting issues (semicolons, quotes, indentation).
- ESLint will run, and since
anotherUnusedVariablecannot be auto-fixed, it will report an error. - The commit will fail because ESLint returned an error.
You will see output in your terminal indicating the ESLint error. Now, manually fix
anotherUnusedVariableby removing it or prefixing with_(e.g.,const _anotherUnusedVariable = ...;). Stage the change again, and try to commit. This time, it should pass!This demonstrates the power of pre-commit hooks: they act as a gatekeeper, ensuring that only high-quality, consistently formatted code enters your repository.
- Husky will trigger the
Mini-Challenge: Enforcing a New Rule
Let’s try to enforce a new, simple ESLint rule and see it in action with our pre-commit hook.
Challenge: Configure ESLint to disallow the use of var keywords, preferring const or let.
- Modify
.eslintrc.cjs: Add a rule to disallowvar. - Introduce a
var: Insrc/App.tsx, introduce avardeclaration (e.g.,var myOldSchoolVariable = 10;). - Attempt to Commit: Stage the changes and try to commit them.
- Observe: What happens? Does the commit fail? What message do you see?
- Fix and Commit: Fix the
vardeclaration and successfully commit.
Hint: The rule for disallowing var is typically no-var. You’ll add it to the rules section of your .eslintrc.cjs file, setting it to 'error'.
Common Pitfalls & Troubleshooting
Even with these powerful tools, you might run into some common issues. Here’s how to tackle them:
ESLint and Prettier Conflicts:
- Symptom: ESLint reports formatting errors that Prettier just fixed, or vice-versa.
- Cause: You haven’t correctly configured
eslint-config-prettieror it’s not the last item in yourextendsarray in.eslintrc.cjs. - Fix: Double-check that
plugin:prettier/recommendedis the very last entry in yourextendsarray. This ensures Prettier’s formatting rules take precedence and disable conflicting ESLint stylistic rules.
Pre-commit Hook Not Running:
- Symptom: You commit changes, and linting/formatting issues are still present, or the hook doesn’t seem to fire at all.
- Cause: Husky might not be properly initialized, or the
.husky/pre-commitscript is incorrect. - Fix:
- Ensure
npx husky initwas run successfully. - Verify the content of
.husky/pre-commitis exactlynpx lint-staged(after thehusky.shsource line). - Check that the
lint-stagedconfiguration inpackage.jsonis syntactically correct and matches your file patterns. - Make sure you’re using
git commitand notgit commit --no-verify(which bypasses hooks).
- Ensure
Lint-staged Runs on All Files (Slow Performance):
- Symptom: Committing a small change takes a long time because all files in the project are being linted/formatted.
- Cause: Incorrect glob patterns in
lint-stagedconfiguration. - Fix: Review your
lint-stagedpatterns inpackage.json. Ensure they are specific enough (e.g.,src/**/*.{ts,tsx}instead of**/*.{ts,tsx}) to target only the relevant files.
IDE (VS Code) Not Auto-fixing/Formatting:
- Symptom: You save a file, but ESLint errors persist, or formatting isn’t applied.
- Cause: VS Code extensions not installed, or settings not configured correctly.
- Fix:
- Verify ESLint and Prettier extensions are installed and enabled.
- Check
.vscode/settings.jsonforeditor.formatOnSaveandeditor.codeActionsOnSavesettings. - Restart VS Code. Sometimes extensions need a refresh.
- Ensure no other formatters are overriding Prettier.
Summary
Phew! You’ve just equipped your React development workflow with some seriously powerful tools. Here’s a quick recap of what we covered:
- Linting (ESLint): We set up ESLint to analyze our TypeScript and React code for errors, enforce best practices, and maintain code quality using plugins like
@typescript-eslint,eslint-plugin-react,eslint-plugin-react-hooks, andeslint-plugin-jsx-a11y. - Formatting (Prettier): We integrated Prettier to automatically format our code according to consistent style rules, eliminating debates over semicolons and indentation.
- ESLint-Prettier Integration: We learned how to make ESLint and Prettier work harmoniously using
eslint-config-prettierandeslint-plugin-prettier, ensuring no conflicts. - IDE Integration: We configured VS Code to automatically lint and format on save, boosting productivity.
- Pre-commit Hooks (Husky & lint-staged): We automated the linting and formatting process using Husky and lint-staged, ensuring that only clean, consistent code makes it into your Git repository before every commit.
By mastering these tools, you’re not just writing code; you’re writing professional-grade, maintainable code that adheres to industry standards. This is a crucial step towards building scalable and collaborative React applications.
In the next chapter, we’ll shift our focus to Build Tooling and Bundlers, understanding how our React code gets transformed from development files into optimized, production-ready assets. Get ready to explore Vite, Webpack, and the magic behind efficient deployments!
References
- ESLint Official Documentation
- Prettier Official Documentation
- Husky Official Documentation
- lint-staged Official Documentation
- Vite Official Documentation
- React.dev Learn Section
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.