DEV Community

Cover image for Setup ESLint, Prettier, Husky with Vite
Leonid Dobrinov
Leonid Dobrinov

Posted on

Setup ESLint, Prettier, Husky with Vite

vite-react-ts-eslint-prettier-husky

This manual shows how to setup Eslint, Prettier and Husky for any project.
For example we will use vite-react-ts template.

ESLint example
eslint

Husky example
husky

Conventional Commits example
conventional_commits

0 Theory

0.1 Problem

  • Working in a team might be challenging if every member has own preference how to write the code and which rules to follow.
  • You might want to follow coding standards and best practices.
  • You might want to have an automated tool to check your accessibility while development, remove or at least highlight unused variables etc.

0.2 Solution

Linter - a program that checks a codebase based on the defined set of rules.

Formatter - a program that formats a codebase based on the defined set of rules.

0.3 ESLint

ESLint statically analyzes your code to quickly find problems. It is built into most text editors and you can run ESLint as part of your continuous integration pipeline.

0.4 Prettier

An opinionated code formatter.

0.5 Husky

A program which executes scripts on git actions.

1 Setup

yarn create vite app -- --template react-ts
Enter fullscreen mode Exit fullscreen mode
cd app
Enter fullscreen mode Exit fullscreen mode
yarn install
Enter fullscreen mode Exit fullscreen mode

2 Install ESLint and Prettier packages

2.1 List of packages

  • @typescript-eslint/eslint-plugin
  • @typescript-eslint/parser
  • eslint
  • eslint-plugin-react
  • eslint-plugin-react-hooks
  • eslint-plugin-react-refresh
  • eslint-plugin-import
  • eslint-import-resolver-typescript
  • eslint-config-prettier
  • eslint-plugin-prettier
  • prettier
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh eslint-plugin-import eslint-import-resolver-typescript eslint-plugin-jsx-a11y eslint-config-prettier eslint-plugin-prettier prettier
Enter fullscreen mode Exit fullscreen mode

2.2 package.json

{
  "name": "app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.13.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@typescript-eslint/eslint-plugin": "^8.14.0",
    "@typescript-eslint/parser": "^8.14.0",
    "@vitejs/plugin-react": "^4.3.3",
    "eslint": "^9.14.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-import-resolver-typescript": "^3.6.3",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-prettier": "^5.2.1",
    "eslint-plugin-react": "^7.37.2",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "globals": "^15.11.0",
    "prettier": "^3.3.3",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.11.0",
    "vite": "^5.4.10"
  }
}
Enter fullscreen mode Exit fullscreen mode

2.3 eslint.config.js

import tseslint from 'typescript-eslint'
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import importPlugin from 'eslint-plugin-import'
import jsxA11y from 'eslint-plugin-jsx-a11y'
import prettier from 'eslint-plugin-prettier/recommended'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    // specify the formats on which to apply the rules below
    files: ['**/*.{ts,tsx}'],
    // use predefined configs in installed eslint plugins
    extends: [
      // js
      js.configs.recommended,
      // ts
      ...tseslint.configs.recommended,
      // react
      react.configs.flat.recommended,
      // import
      importPlugin.flatConfigs.recommended,
      // a11y (accessibility
      jsxA11y.flatConfigs.recommended,
      // prettier
      prettier
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser
    },
    // specify used plugins
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh
    },
    settings: {
      // for eslint-plugin-react to auto detect react version
      react: {
        version: 'detect'
      },
      // for eslint-plugin-import to use import alias
      'import/resolver': {
        typescript: {
          project: './tsconfig.json'
        }
      }
    },
    rules: {
      // set of custom rules
      'no-console': 'warn',
      'react/button-has-type': 'error',
      'react/react-in-jsx-scope': ['off'],
      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

2.4 extends vs plugins

When you use extends the predefined config is loaded and you can add/change the rules in rules.
When you use plugins the predefined config is NOT loaded, and you need to define all of the rules for this plugin. Usually you use plugins if the plugin doesn't have the defined config.

2.5 eslint-config-airbnb

Eslint v9 uses flat config (new method of writing eslint config found to add more flexibility when configuring eslint), as for now eslint-config-airbnb can't be used with eslint v9. There are some workarounds but we will not use them.

2.6 .prettierrc

{
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 100,
  "semi": false,
  "arrowParens": "always"
}
Enter fullscreen mode Exit fullscreen mode

3 Testing eslint work

3.1 settings.json

"editor.formatOnSave": true,
Enter fullscreen mode Exit fullscreen mode

3.2 Reload VSCode

src/App.tsx

import { useState } from 'react'
import reactLogo from './assets/react.svg'
// Unable to resolve path to module '/vite.svg'.eslintimport/no-unresolved
import viteLogo from '/public/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)
  // 'test' is assigned a value but never used.eslint@typescript-eslint/no-unused-vars
  const test = 123
  console.log('aaa')

  const f = [1, 2, 3, 4, 5].filter((n) => n % 2 === 0)

  return (
    <>
      <div>
        {/* Using target="_blank" without rel="noreferrer" (which implies rel="noopener") is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendationseslintreact/jsx-no-target-blank */}
        <a href="https://vite.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>
      {/* img elements must have an alt prop, either with meaningful text, or an empty string for decorative images.eslintjsx-a11y/alt-text */}
      <img src="" />
      <h1>
        {/* * img elements must have an alt prop, either with meaningful text, or an empty string for decorative 
Missing an explicit type attribute for buttoneslintreact/button-has-type */}
        <button>test</button>
      </h1>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>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
Enter fullscreen mode Exit fullscreen mode

eslint

3.3 Adding ESLint to existing codebase

If you add ESLint to a codebase where ESLint wasn't used before you might get many errors. What you can do is

# fix all files
yarn lint --fix
Enter fullscreen mode Exit fullscreen mode
# fix individual files
yarn eslint --fix /src/Comp.tsx
Enter fullscreen mode Exit fullscreen mode

4 ESLint Prettier Jest TestingLibrary Vitest

4.1 Install packages

yarn add -D eslint-plugin-jest eslint-plugin-testing-library eslint-plugin-vitest
Enter fullscreen mode Exit fullscreen mode

4.2 package.json

{
  "name": "app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.13.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@typescript-eslint/eslint-plugin": "^8.14.0",
    "@typescript-eslint/parser": "^8.14.0",
    "@vitejs/plugin-react": "^4.3.3",
    "eslint": "^9.14.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-import-resolver-typescript": "^3.6.3",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-jest": "^28.9.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-prettier": "^5.2.1",
    "eslint-plugin-react": "^7.37.2",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "eslint-plugin-testing-library": "^6.4.0",
    "eslint-plugin-vitest": "^0.5.4",
    "globals": "^15.11.0",
    "prettier": "^3.3.3",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.11.0",
    "vite": "^5.4.10"
  }
}
Enter fullscreen mode Exit fullscreen mode

4.3 eslint.config.js

import tseslint from 'typescript-eslint'
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import importPlugin from 'eslint-plugin-import'
import jsxA11y from 'eslint-plugin-jsx-a11y'
import prettier from 'eslint-plugin-prettier/recommended'
import jest from 'eslint-plugin-jest'
import testingLibrary from 'eslint-plugin-testing-library'
import vitest from 'eslint-plugin-vitest'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      ...tseslint.configs.recommended,
      react.configs.flat.recommended,
      importPlugin.flatConfigs.recommended,
      jsxA11y.flatConfigs.recommended,
      prettier
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh
    },
    settings: {
      react: {
        version: 'detect'
      },
      'import/resolver': {
        typescript: {
          project: './tsconfig.json'
        }
      }
    },
    rules: {
      'no-console': 'warn',
      'react/button-has-type': 'error',
      'react/react-in-jsx-scope': ['off'],
      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
    }
  },
  {
    files: ['**/*.{spec,test}.{ts,tsx}'],
    extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
    plugins: { jest, 'testing-library': testingLibrary, vitest },
    languageOptions: {
      globals: jest.environments.globals.globals
    },
    rules: {
      'jest/no-disabled-tests': 'warn',
      'jest/no-focused-tests': 'error',
      'jest/no-identical-title': 'error',
      'jest/prefer-to-have-length': 'warn',
      'jest/valid-expect': 'error',
      'testing-library/await-async-queries': 'error',
      'testing-library/no-await-sync-queries': 'error',
      'testing-library/no-debugging-utils': 'warn',
      'testing-library/no-dom-import': 'off',
      ...vitest.configs.recommended.rules,
      'vitest/max-nested-describe': ['error', { max: 3 }]
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

5 Husky pre-commit hook

Husky
Ultra-fast modern native git hooks

lint-staged
Run linters against staged git files and don't let 💩 slip into your code base!

5.1 Install packages

yarn add -D husky lint-staged
Enter fullscreen mode Exit fullscreen mode
npx husky init
Enter fullscreen mode Exit fullscreen mode

5.2 app/.husky/pre-commit

npx lint-staged

5.3 app/lint-staged.config.js

export default {
  '**/*.{ts,tsx}': (stagedFiles) => [`eslint .`, `prettier --write ${stagedFiles.join(' ')}`]
}
Enter fullscreen mode Exit fullscreen mode

husky

6 Conventional commits

link

6.1 Install packages

yarn add -D @commitlint/cli @commitlint/config-conventional
Enter fullscreen mode Exit fullscreen mode

6.2 app/.husky/commit-msg

npx --no-install commitlint --edit "$1"

6.3 app/commitlint.config.js

export default { extends: ['@commitlint/config-conventional'] }
Enter fullscreen mode Exit fullscreen mode

conventional_commits

React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

  • Configure the top-level parserOptions property like this:
export default tseslint.config({
  languageOptions: {
    // other options...
    parserOptions: {
      project: ['./tsconfig.node.json', './tsconfig.app.json'],
      tsconfigRootDir: import.meta.dirname
    }
  }
})
Enter fullscreen mode Exit fullscreen mode
  • Replace tseslint.configs.recommended to tseslint.configs.recommendedTypeChecked or tseslint.configs.strictTypeChecked
  • Optionally add ...tseslint.configs.stylisticTypeChecked
  • Install eslint-plugin-react and update the config:
// eslint.config.js
import react from 'eslint-plugin-react'

export default tseslint.config({
  // Set the react version
  settings: { react: { version: '18.3' } },
  plugins: {
    // Add the react plugin
    react
  },
  rules: {
    // other rules...
    // Enable its recommended rules
    ...react.configs.recommended.rules,
    ...react.configs['jsx-runtime'].rules
  }
})
Enter fullscreen mode Exit fullscreen mode

Top comments (0)