In modern web development, maintaining code quality and consistency is paramount. This article outlines how to migrate to the latest ESLint v9, integrate Prettier for code formatting, and leverage lint-staged to automate these checks before each commit. We'll assume you already have Husky set up for Git hooks.
- Node.js (>=22.4.1)
- npm or yarn
- Git
- Husky (already installed)
First, update your ESLint packages and install the necessary plugins. Here's the relevant section from package.json:
{
"devDependencies": {
"@eslint/cli": "^9.0.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"eslint": "^9.35.0",
"eslint-config-next": "^15.5.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"prettier": "^3.6.2",
"lint-staged": "^16.1.6"
},
"overrides": {
"eslint": "$eslint"
}
}Run the following command to update/install:
npm install --save-dev @eslint/cli @eslint/eslintrc @eslint/js @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-next eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier lint-stagedMigrate your ESLint configuration to the flat config format (eslint.config.mjs). This is the recommended format for ESLint v9.
Here's an example eslint.config.mjs:
import js from '@eslint/js'
import tsParser from '@typescript-eslint/parser'
import globals from 'globals'
import { FlatCompat } from '@eslint/eslintrc'
import prettier from 'eslint-plugin-prettier' // Add Prettier plugin
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
recommendedConfig: js.configs.recommended,
})
const eslintconfig = [
// Global ignores
{
ignores: [
'**/.eslintrc.json',
'**/tsconfig.json',
'**/index.d.ts',
'**/*.config.js',
'.next/',
'out/',
'node_modules/',
'public/',
'scripts/update-version.js',
'./next-env.d.ts',
'coverage/',
'__tests__/',
'redux/',
],
},
// Base JS configuration
js.configs.recommended,
// Prettier integration
{
plugins: {
prettier, // Enable Prettier as an ESLint plugin
},
rules: {
'prettier/prettier': 'error', // Run Prettier as an ESLint rule
},
},
// Next.js and other plugins using compat
...compat.extends(
'next/core-web-vitals',
'next/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:prettier/recommended' // Adds Prettier config to disable conflicting rules
),
// Main configuration for all files
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
ecmaVersion: 2020,
sourceType: 'module',
},
settings: {
react: {
version: 'detect',
},
},
rules: {
// Your existing rules (keep non-stylistic ones)
'react/prop-types': 'off',
'react/forbid-prop-types': 'error',
'react/default-props-match-prop-types': 'error',
'react/self-closing-comp': 'error',
'react/no-unused-prop-types': 'error',
'react/jsx-key': 'error',
'react/no-unused-state': 'error',
'react/state-in-constructor': 'error',
'react/function-component-definition': 'off',
'react/require-default-props': 'off',
'react/no-array-index-key': 'off',
'react/no-unescaped-entities': 'off',
'react/no-unstable-nested-components': 'off',
'react/no-danger': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/alt-text': 'error',
'jsx-a11y/mouse-events-have-key-events': 'error',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-a11y/no-noninteractive-element-interactions': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/interactive-supports-focus': 'off',
'import/first': 'error',
'import/no-mutable-exports': 'error',
'import/no-useless-path-segments': 'error',
'import/no-named-as-default': 'error',
'import/no-duplicates': 'error',
'import/newline-after-import': 'error',
'import/no-extraneous-dependencies': 'off',
'import/order': 'off',
'import/no-cycle': 'off',
'import/extensions': 'off',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-bitwise': 'off',
'no-underscore-dangle': 'off',
'no-nested-ternary': 'off',
'no-restricted-syntax': 'off',
'no-unused-vars': 'off',
'no-return-await': 'off',
// Remove stylistic rules (handled by Prettier)
'react/jsx-indent-props': 'off',
'react/jsx-curly-newline': 'off',
'react/jsx-equals-spacing': 'off',
'react/jsx-indent': 'off',
'react/jsx-props-no-multi-spaces': 'off',
'react/jsx-curly-brace-presence': 'off',
'react/jsx-closing-bracket-location': 'off',
'react/jsx-tag-spacing': 'off',
'react/jsx-props-no-spreading': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-closing-tag-location': 'off',
'react/jsx-boolean-value': 'off',
'react/jsx-wrap-multilines': 'off',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-one-expression-per-line': 'off',
'react/jsx-max-props-per-line': 'off',
'react/jsx-curly-spacing': 'off',
'object-curly-spacing': 'off',
'space-before-blocks': 'off',
'keyword-spacing': 'off',
'comma-spacing': 'off',
'space-infix-ops': 'off',
'quote-props': 'off',
'function-paren-newline': 'off',
'function-call-argument-newline': 'off',
'spaced-comment': 'off',
'operator-linebreak': 'off',
'computed-property-spacing': 'off',
'array-callback-return': 'off',
'space-unary-ops': 'off',
'object-shorthand': 'off',
'key-spacing': 'off',
'prefer-const': 'off',
'prefer-destructuring': 'off',
'prefer-template': 'off',
'prefer-regex-literals': 'off',
'prefer-promise-reject-errors': 'off',
'guard-for-in': 'off',
'no-cond-assign': 'off',
'no-sequences': 'off',
'no-unneeded-ternary': 'off',
'no-extra-boolean-cast': 'off',
'no-lonely-if': 'off',
'no-unsafe-optional-chaining': 'off',
'no-mixed-operators': 'off',
'no-confusing-arrow': 'off',
'no-plusplus': 'off',
'no-constant-condition': 'off',
'no-floating-decimal': 'off',
'eol-last': 'off',
'array-bracket-spacing': 'off',
'space-in-parens': 'off',
'template-curly-spacing': 'off',
'no-tabs': 'off',
'dot-notation': 'off',
'implicit-arrow-linebreak': 'off',
'padded-blocks': 'off',
'no-multiple-empty-lines': 'off',
'no-multi-spaces': 'off',
'no-else-return': 'off',
indent: 'off',
'linebreak-style': 'off',
semi: 'off',
'no-param-reassign': 'off',
'jsx-quotes': 'off',
'max-len': 'off',
'object-curly-newline': 'off',
'arrow-body-style': 'off',
'arrow-parens': 'off',
'consistent-return': 'off',
eqeqeq: 'off',
'no-trailing-spaces': 'off',
'no-useless-return': 'off',
},
},
// TypeScript-specific configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
'@typescript-eslint/return-await': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-expressions': 'warn',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/semi': 'off',
'@typescript-eslint/comma-dangle': 'off',
'@typescript-eslint/quotes': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-var-requires': 'off',
},
},
]
export default eslintconfigCreate a .prettierrc.js or .prettierrc.json file to configure Prettier's formatting rules. A basic example:
// .prettierrc.js
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'all',
printWidth: 120,
tabWidth: 2,
};Also, create a .prettierignore file to specify files and directories that Prettier should ignore:
node_modules/
.next/
out/
public/
scripts/update-version.js
next-env.d.ts
coverage/
__tests__/
redux/
*.config.js
tsconfig.json
.eslintrc.json
index.d.ts
Configure lint-staged in your package.json to run ESLint and Prettier on staged files before committing:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}
}Since you already have Husky set up, modify the pre-commit hook to use lint-staged. If you don't have it, install husky using npm install husky --save-dev and enable git hooks husky install.
Your .husky/pre-commit file should contain:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-stagedMake sure the pre-commit file is executable:
chmod +x .husky/pre-commitIf you're using Commitlint, ensure it's configured correctly. The commitlint.config.js file might look like this:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [0] // Disable case checking entirely
}
};And the commit-msg hook in .husky should point to commitlint:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"Now, try making a change to a JavaScript or TypeScript file, stage it, and commit. lint-staged will automatically run ESLint and Prettier on the staged files.
git add .
git commit -m "Test: Test ESLint, Prettier, and lint-staged integration"- ESLint errors not being fixed: Double-check your ESLint configuration and ensure that the rules are correctly set up.
- Prettier not formatting: Verify your Prettier configuration and ensure that it's not conflicting with ESLint rules.
- lint-staged not running: Make sure Husky is properly installed and the
pre-commithook is correctly configured. Also, verify that the file extensions inlint-stagedmatch your project's file types. - Conflicting rules: ESLint and Prettier can sometimes have conflicting rules. Use
eslint-config-prettierto disable ESLint rules that conflict with Prettier.
By integrating ESLint v9, Prettier, and lint-staged with Husky, you can create a robust and automated workflow that ensures code quality and consistency across your project. This setup helps catch errors early, enforce coding standards, and improve collaboration among