I still remember the “tabs vs. spaces” wars. They weren’t fun. But honestly? The “formatting in CI” wars were worse. You know the drill: you push a quick fix at 5:30 PM, pack up your bag, and by the time you get to the elevator, your phone buzzes. The build failed. Why? Because you missed a semicolon or used single quotes instead of double.

It’s infuriating.

Well, that’s not entirely accurate — I spent last Tuesday migrating a legacy repo to the modern stack—React 19, ESLint 9 (Flat Config), and Prettier—and I realized just how much the tooling landscape has shifted since the old .eslintrc days. If you’re still copy-pasting config files from 2023, you’re probably seeing a lot of deprecation warnings in your terminal. As discussed in TypeScript Won. Now Stop Complaining and Fix Your Config, the modern JavaScript ecosystem has evolved rapidly, and it’s important to keep your tooling up-to-date.

Here is my current, battle-tested setup for 2026. No fluff, just the config that actually works on my machine (Node 22.13.0, for the record).

The “Flat Config” Reality Check

The biggest hurdle right now isn’t React; it’s ESLint. And if you haven’t touched your linting setup in a year or two, you might not know that the old configuration format is basically dead. ESLint 9 made “Flat Config” the default, and it broke a lot of muscle memory. As mentioned in The State of Bundling: Mastering Webpack 5 in the Modern JavaScript Ecosystem, the JavaScript tooling landscape is constantly evolving, and it’s important to stay up-to-date.

But after converting three projects, I admit it: the new system is actually logical. It treats configuration as actual JavaScript code rather than a static JSON blob, which means we can finally import plugins properly without relying on magic string resolution.

React JavaScript code screen - Programming language on black screen background javascript react ...
React JavaScript code screen – Programming language on black screen background javascript react …

Let’s start fresh. Assuming you have a React project (I’m using Vite 6.1 here), you need these dev dependencies. I’m being specific with versions because I’m tired of vague tutorials breaking a month later.

npm install --save-dev eslint@9.18.0 prettier@3.4.2 husky@9.1.7 lint-staged@15.3.0 eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh globals

Configuring ESLint (The New Way)

Delete your .eslintrc.json. Seriously, get rid of it. Create a file named eslint.config.js in your root. Note the lack of a leading dot.

Here is the config I’m running on my production dashboard right now. It handles React 19 hooks, JSX runtime rules, and plays nice with Prettier without needing the old eslint-config-prettier extend (mostly).

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';

export default [
  { ignores: ['dist', 'node_modules'] },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2022,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    settings: { react: { version: '19.0' } },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...js.configs.recommended.rules,
      ...react.configs.recommended.rules,
      ...react.configs['jsx-runtime'].rules,
      ...reactHooks.configs.recommended.rules,
      'react/jsx-no-target-blank': 'off',
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      // My personal preference: strict on unused vars but ignore underscore-prefixed ones
      'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    },
  },
];

Prettier: The Formatting Hammer

I don’t argue about code style anymore. I let Prettier do it. The only decision I make is creating a .prettierrc file so my team doesn’t kill me over tab width. As outlined in How I Configure Prettier for Modern JavaScript in 2025, having a consistent code style is crucial for maintainability.

{
  "semi": true,
  "tabWidth": 2,
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all",
  "jsxSingleQuote": false
}

Husky: Because You Will Forget

The goal is to catch the mess before it leaves your laptop. Husky intercepts your git commit. It’s the bouncer at the door. Setting it up used to be a multi-step nightmare in v4, but v9 is clean. Run this once:

software developer workstation - Software developer workstation Photos - Download Free High-Quality ...
software developer workstation – Software developer workstation Photos – Download Free High-Quality …
npx husky init

The solution is lint-staged. It only checks the files you actually changed. This approach is similar to the one discussed in Building Robust Real-Time Features with AdonisJS and Socket.IO, where the author emphasizes the importance of catching issues early in the development process.

Does It Actually Work?

If I try to commit this right now:

git add src/BadComponent.jsx
git commit -m "Add bad component"

Result: The commit fails immediately. Husky blocks it. My terminal screams at me about x being assigned but never used, and warns about the security risk of target="_blank" without rel="noreferrer". Prettier might fix the spacing, but ESLint throws the error.