Skip to content

14 - ESLint ​


What is ESLint ​

A static analysis tool that finds and fixes problems in JavaScript/TypeScript code. It catches bugs, enforces code style, and ensures consistency across a team.

What it catches:

  • Unused variables
  • Missing dependencies in useEffect
  • Accessibility issues in JSX
  • Type-unsafe patterns
  • Import order violations
  • Potential runtime errors (e.g., === vs ==)

ESLint Flat Config (v9+) — The New Standard ​

ESLint v9 introduced "flat config" (eslint.config.js) replacing the old .eslintrc.* format. Vite projects now scaffold with flat config by default.

Default Vite + React + TS config ​

js
// 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: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
);

What each piece does ​

PackagePurpose
@eslint/jsCore JavaScript rules (no-unused-vars, no-undef, etc.)
typescript-eslintTypeScript-specific rules (type safety, generics, etc.)
eslint-plugin-react-hooksEnforces Rules of Hooks (deps array, hook call order)
eslint-plugin-react-refreshEnsures components are compatible with Fast Refresh
globalsProvides global variable definitions (browser, node)

Key Rules to Know ​

JavaScript / General ​

js
rules: {
  'no-unused-vars': 'warn',          // flag unused variables
  'no-console': 'warn',              // flag console.log (remove before prod)
  'prefer-const': 'error',           // use const when not reassigned
  'eqeqeq': 'error',                // require === instead of ==
  'no-var': 'error',                 // use let/const, not var
  'curly': 'error',                  // require braces for if/else/for/while
}

TypeScript ​

js
rules: {
  '@typescript-eslint/no-unused-vars': ['warn', {
    argsIgnorePattern: '^_',         // allow _unused params
  }],
  '@typescript-eslint/no-explicit-any': 'warn',   // flag `any` type
  '@typescript-eslint/no-non-null-assertion': 'warn', // flag `value!`
  '@typescript-eslint/consistent-type-imports': 'error', // import type { X }
}

React Hooks (Critical) ​

js
// These come from eslint-plugin-react-hooks
rules: {
  'react-hooks/rules-of-hooks': 'error',
  // Enforces:
  // - Only call hooks at the top level (not inside loops/conditions)
  // - Only call hooks from React functions

  'react-hooks/exhaustive-deps': 'warn',
  // Enforces:
  // - All variables used inside useEffect/useMemo/useCallback
  //   must be in the dependency array
}
tsx
// rules-of-hooks: ERROR
function Component({ showExtra }) {
  if (showExtra) {
    const [val, setVal] = useState(0); // hooks can't be conditional!
  }
}

// exhaustive-deps: WARNING
function Component({ userId }) {
  useEffect(() => {
    fetchUser(userId); // uses userId
  }, []); // missing userId in deps array!
  // Fix: [userId]
}

Adding Extra Plugins ​

eslint-plugin-import (import order and validation) ​

bash
npm install -D eslint-plugin-import
js
// In eslint.config.js
import importPlugin from 'eslint-plugin-import';

// Add to rules:
rules: {
  'import/order': ['error', {
    groups: [
      'builtin',       // node built-ins (path, fs)
      'external',      // npm packages (react, zustand)
      'internal',      // @ aliases (@/components)
      'parent',        // ../
      'sibling',       // ./
      'index',         // ./ index file
    ],
    'newlines-between': 'always',
    alphabetize: { order: 'asc' },
  }],
  'import/no-duplicates': 'error',
}

Result:

tsx
// External packages first
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';

// Internal aliases
import { Button } from '@/components/Button';

// Relative imports
import { formatCurrency } from '../utils/format';
import { useCheckout } from './hooks';

eslint-plugin-jsx-a11y (Accessibility) ​

bash
npm install -D eslint-plugin-jsx-a11y
js
import jsxA11y from 'eslint-plugin-jsx-a11y';

// Extends:
extends: [jsxA11y.configs.recommended],

Catches:

tsx
// ERROR: img without alt
<img src="photo.jpg" />
// Fix: <img src="photo.jpg" alt="Team photo" />

// ERROR: onClick on non-interactive element without role
<div onClick={handleClick}>Click me</div>
// Fix: <button onClick={handleClick}>Click me</button>

// ERROR: form input without label
<input type="text" />
// Fix: <label>Name <input type="text" /></label>

Running ESLint ​

bash
# Lint all files
npx eslint .

# Lint specific files
npx eslint src/components/

# Fix auto-fixable issues
npx eslint . --fix

# Show only errors (ignore warnings)
npx eslint . --quiet

package.json scripts ​

json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

ESLint + VS Code ​

Install the ESLint extension. Add to .vscode/settings.json:

json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact"]
}

Now ESLint auto-fixes on every save.


Old Config vs Flat Config (Know the Difference) ​

Interviewers might have codebases using either format.

Old format (.eslintrc.json / .eslintrc.js) — ESLint < 9 ​

json
{
  "env": { "browser": true, "es2021": true },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "@typescript-eslint"],
  "rules": {
    "no-unused-vars": "warn"
  }
}

New format (eslint.config.js) — ESLint 9+ ​

js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    rules: {
      'no-unused-vars': 'warn',
    },
  },
];

Key differences:

  • Flat config: array of config objects, no extends string magic
  • Flat config: plugins are imported directly (no string names)
  • Flat config: ignores replaces .eslintignore
  • Flat config: no need for separate parser config (typescript-eslint handles it)

Frontend interview preparation reference.