Skip to main content
All posts

Keeping Team Code Consistent with ESLint, Prettier, Husky, and lint-staged

Bosh9 min read

Introduction​

Our projects at work didn't have a shared set of code style and formatting rules, which made collaboration and maintenance harder than they needed to be. Everyone had their own coding style, formatting preferences, and editor formatter, so everyone's auto-formatting produced slightly different results. Many of us also used VSCode's formatOnSave. It's convenient, but it would often reformat code unrelated to the actual change, and those formatting changes would end up in the same commit. That extra noise made reviews harder.

To improve things, I introduced ESLint, Prettier, Husky, and lint-staged to give the team a consistent style and format. The idea was to apply the same rules while we edited, then check the code before committing, so everything pushed to GitHub or GitLab followed the same conventions. That would help us maintain code quality and work more efficiently as a team. I wrote down the setup process and the resources I used so I could follow the same steps when starting a new project.

The Tools​

Here's a quick overview of each tool and how they work together:

  1. ESLint: A static analysis tool that checks JavaScript for errors and style issues. You can customize its rules in an .eslintrc configuration file at the project root.
  2. Prettier: A formatter that automatically handles indentation, spacing, quotes, and other formatting details. You can customize it in a .prettierrc file at the project root.
  3. Husky: A Git hooks tool for setting up hooks such as pre-commit and pre-push, so specified scripts run before developers commit or push.
  4. lint-staged: A Git hooks tool that runs tasks such as linting and formatting on selected files during a Git pre-commit hook, making sure the code follows the agreed rules.

ESLint​

ESLint statically analyzes JavaScript for errors and style issues, helping developers catch and fix problems as they work. You can configure it through a file whose name starts with .eslintrc, choosing which rules to check and which files or directories to ignore. You can also exclude files and folders from linting with .eslintignore.

Installing and Creating a Basic Config​

Follow the official ESLint Quick start and run:

npm init @eslint/config

The CLI asks a few questions to help configure .eslintrc:

✔ How would you like to use ESLint? · style

✔ What type of modules does your project use? · esm

✔ Which framework does your project use? · react

✔ Does your project use TypeScript? · No / Yes

✔ Where does your code run? · browser

✔ How would you like to define a style for your project? · guide

✔ Which style guide do you want to follow? · airbnb

✔ What format do you want your config file to be in? · JSON

Once you've answered, it generates a configuration file starting with .eslintrc. For example:

# .eslintrc.json
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"airbnb"
],
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
}
}

Common .eslintrc Options​

.eslintrc configures ESLint. Some common options are:

  • env: The environments your code runs in, such as browser, node, or es6.
  • parser: The JavaScript parser to use, such as babel-eslint or @typescript-eslint/parser.
  • extends: The ESLint configurations to inherit from, including the built-in eslint:recommended or third-party configurations.
  • plugins: ESLint plugins to load, such as eslint-plugin-react or eslint-plugin-vue.
  • rules: Rules to apply. These can override inherited settings or define additional rules, such as no-console and no-unused-vars.
tip

💡 Both extends and plugins extend ESLint, but they serve different purposes:

  • extends loads an existing configuration, whether the built-in eslint:recommended or a third-party config. Use it when you want a complete, ready-made set of settings to start from.
  • plugins loads ESLint plugins such as eslint-plugin-react and eslint-plugin-vue. Use plugins to bring in rules for a particular area, such as React or Vue, so ESLint can check that code more precisely.

Installing eslint-config-prettier​

Linters usually include both code quality rules and stylistic rules. Most stylistic rules become unnecessary when you use Prettier, and some can even conflict with it. Both ESLint and Prettier recommend including eslint-config-prettier in your ESLint configuration. It disables overlapping ESLint rules, leaving ESLint to handle checks that Prettier doesn't cover.

  • Install it:
yarn add -dev eslint-config-prettier
  • Remember to add prettier to extends in every .eslintrc, because a child directory's .eslintrc can override the parent configuration. See @ESLint - Cascading and Hierarchy. Put it last in the extends array so it can override the other configurations.
// .eslintrc
{
"extends": [
...
"prettier"
]
}

Prettier​

Prettier automatically formats code to make it consistent and easier to read. It helps avoid the inconsistencies that come from everyone using different editors and coding styles. A configuration file starting with .prettierrc lets you define formatting preferences, while .prettierignore excludes files or folders you don't want formatted. Together, these let you adapt Prettier to the team's needs and habits and reduce formatting-related problems.

Install​

yarn add -D prettier @trivago/prettier-plugin-sort-imports
  • Add a .prettierignore file.
  • Add formatting commands to the scripts in package.json:
"format": "prettier --check \"./src/**/*.js\"",
"format-write": "prettier --write --ignore-unknown \"./src/**/*.js\"",

Common .prettierrc Options​

// .prettierrc
{
"semi": false, // Whether to add semicolons at the end of statements.
"singleQuote": false, // Whether to use single or double quotes.
"tabWidth": 2, // Set the indentation width.
"trailingComma": "all", // Whether to add trailing commas in multiline code.
"arrowParens": "always", // Whether to wrap arrow function parameters in parentheses.
"bracketSameLine": false // Whether to keep > on the last line of the opening tag or put it on a new line.
}

For more options, see the official documentation: @Prettier - Options.

tip

If auto-formatting stops working in VSCode, there's often a problem with a Prettier setting or package. Check the Output panel for errors. For example, I tried installing prettier-plugin-organize-imports, and Prettier stopped responding afterward. It turned out that TypeScript was a peer dependency of the plugin.

prettier-plugin-sort-imports​

prettier-plugin-sort-imports is a Prettier plugin for defining the order of import statements. You specify the order with regular expressions, and matching modules are sorted accordingly. By default, the plugin puts third-party packages at the top. To move them elsewhere in the order, use <THIRD_PARTY_MODULES>.

// .prettierrc.js
// React packages >> third-party packages >> @core paths >> @server paths >> ~ paths >> paths starting with . or /
{
"importOrder": [
'^(^react$|@react|react)',
"<THIRD_PARTY_MODULES>",
'^@core/(.*)$',
'^@server/(.*)$',
'^@ui/(.*)$',
"^~/(.*)$",
'^[./]',
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}

Husky + lint-staged​

Husky manages Git hooks and runs scripts at points such as before, during, and after a commit. It lets you choose which checks and formatting tasks to run before code is committed. lint-staged runs lint checks only on the code about to be committed. It filters those files using patterns, checks them, and reports the results, helping developers catch errors and style problems before committing.

These tools work together: Husky triggers lint-staged before a commit, and a failed check blocks the commit. Specifically, you run lint-staged in Husky's pre-commit hook, pass the result back to Husky, and let the hook determine whether the commit can proceed.

Install​

# Install Husky and lint-staged
yarn add --dev husky lint-staged
# Initialize Husky and create default Git hooks in the project.
npx husky install
# Set up a script to run husky install automatically before npm install or npm ci.
npm pkg set scripts.prepare="husky install"
# Add a Git hook to run lint-staged before each commit.
npx husky add .husky/pre-commit "npx lint-staged"

Configuring .lintstagedrc​

{
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": "eslint --fix",
"src/**/*.{js,jsx,ts,tsx,css,scss,md}": "prettier --write --ignore-unknown"
}
}

Before a commit, this configuration first runs eslint --fix on the js, jsx, ts, and tsx files in src. If the command reports an error, the commit is blocked. It then runs prettier --write --ignore-unknown on the js, jsx, ts, tsx, css, scss, and md files in src. This gives everyone on the team the same formatting and lint rules for their committed code.

tip

💡 The Prettier documentation notes that when lint-staged runs ESLint, ESLint should run before Prettier.


Reference​