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 ​
| Package | Purpose |
|---|---|
@eslint/js | Core JavaScript rules (no-unused-vars, no-undef, etc.) |
typescript-eslint | TypeScript-specific rules (type safety, generics, etc.) |
eslint-plugin-react-hooks | Enforces Rules of Hooks (deps array, hook call order) |
eslint-plugin-react-refresh | Ensures components are compatible with Fast Refresh |
globals | Provides 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-importjs
// 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-a11yjs
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 . --quietpackage.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
extendsstring magic - Flat config: plugins are imported directly (no string names)
- Flat config:
ignoresreplaces.eslintignore - Flat config: no need for separate parser config (typescript-eslint handles it)