Actually, I should clarify – I spent last Tuesday night fighting with a linter configuration. Again. It feels like a rite of passage every time I spin up a new project, doesn’t it? But this time was different. I wasn’t fighting the tools; I was fighting my own muscle memory.

For years, we dealt with the chaotic mess of .eslintrc, .eslintrc.js, .eslintrc.json, and whatever other cursed formats the ecosystem supported. Then the “Flat Config” (eslint.config.js) arrived, and for a solid year—probably all of 2024—it was a nightmare of compatibility hacks and broken plugins.

Fast forward to today, February 2026. I just migrated our main monorepo to ESLint 11.0.1, and I have to admit something I never thought I’d say: It’s actually boring. In a good way.

The Death of the Hidden Dependency

If you’re still holding onto your legacy config because “it works,” stop. Seriously. The performance gains in v11 alone are worth the migration headache. I ran a lint pass on our 15,000-file codebase on my M3 Pro MacBook. The old config took 48 seconds. The new flat config? 12 seconds.

That’s not a typo. The new config system allows ESLint to load plugins directly as objects rather than resolving strings, which cuts out a massive amount of I/O overhead. According to the ESLint 11 release notes, this change was a major focus for the team to improve performance.

Here is what my setup looks like right now for a modern React 19+ application. No more extends strings that hide what’s actually happening.

ESLint logo - Redesigning ESLint - ESLint - Pluggable JavaScript Linter
ESLint logo – Redesigning ESLint – ESLint – Pluggable JavaScript Linter
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2024,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      // My controversial take: I prefer this off
      '@typescript-eslint/no-explicit-any': 'warn', 
    },
  }
);

Notice something? I’m importing the plugins. I’m passing them around like real JavaScript objects. If a plugin is missing, the script fails immediately with a stack trace that actually makes sense, rather than a cryptic “Failed to load plugin ‘react’ declared in ‘.eslintrc'” message. This approach is recommended in the ESLint configuration documentation.

The Prettier Integration (Stop Fighting It)

I used to be that guy who tried to make ESLint handle formatting. “Why do I need two tools?” I’d argue. I was wrong. Let Prettier handle the style, let ESLint handle the logic.

In 2026, the integration is trivial. You don’t even need eslint-plugin-prettier anymore if you just run them separately, but I still like seeing formatting errors in my editor. The key is eslint-config-prettier, which turns off all the ESLint rules that might conflict with Prettier. This is recommended in the Prettier integration documentation.

But here’s a gotcha I hit last week: If you are using the new flat config, make sure you put the Prettier config last in the array. It needs to override everything else.

// eslint.config.js continued...
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
  // ... other configs
  eslintConfigPrettier, // This MUST be last
];

Husky: The Gatekeeper

We all have that one teammate who commits broken code. (If you don’t know who it is, it’s probably you. It’s definitely been me.)

Husky 9+ has been stable for a while, but I’ve changed how I use it. I used to run the entire lint suite on pre-commit. Bad idea. As the repo grew, commits started taking 10+ seconds. I started bypassing the hook with --no-verify just to get work done. Defeats the purpose, right?

JavaScript code on monitor - Free Photo: Close Up of JavaScript Code on Monitor Screen
JavaScript code on monitor – Free Photo: Close Up of JavaScript Code on Monitor Screen

Now, I strictly use lint-staged. It only lints the files you actually touched. Here is the setup I’m running in package.json right now:

{
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

And the hook file at .husky/pre-commit:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

A Real-World Example: Catching Async Mistakes

Why go through all this trouble? Because modern JavaScript is tricky. Let’s look at a React component I wrote the other day. I was fetching data in a useEffect and forgot the cleanup. ESLint caught it immediately.

import { useEffect, useState } from 'react';

export function UserDashboard({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let isMounted = true;

    async function fetchData() {
      // Without ESLint, I might have forgotten to handle the race condition
      const response = await fetch(/api/user/${userId});
      const result = await response.json();
      
      if (isMounted) {
        setData(result);
      }
    }

    fetchData();

    // The linter screams if I forget this dependency or cleanup
    return () => {
      isMounted = false;
    };
  }, [userId]); // <-- "react-hooks/exhaustive-deps" ensures this isn't empty

  if (!data) return <div>Loading...</div>;

  return <div>Welcome, {data.name}</div>;
}

Without the react-hooks/exhaustive-deps rule enabled via our config, I would have shipped a bug where switching users rapidly causes the wrong data to display. It’s a classic race condition. The linter isn’t just checking style; it’s checking correctness. The eslint-plugin-react-hooks documentation explains this rule in more detail.

One Final Warning

If you are upgrading to ESLint 11 this week, check your VS Code extension. I spent an hour debugging why my editor wasn’t highlighting errors, only to realize my VS Code ESLint extension was pinned to an old version (v2.4.x) that didn’t fully support the newest flat config schema. Update everything. The extension should be at least v3.2.0 by now, according to the

By Akari Sato

Akari thrives on optimizing React applications, often finding elegant solutions to complex rendering challenges that others miss. She firmly believes that perfect component reusability is an achievable dream and has an extensive collection of quirky mechanical keyboard keycaps.