Skip to main content

Overview

The SAAC Frontend template uses ESLint 9.x with the new flat config format (eslint.config.js). This modern configuration format provides better performance and simpler composition.

Configuration File

The ESLint configuration is defined in eslint.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'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])
This is ESLint’s flat config format (not the legacy .eslintrc.js). Flat configs are simpler, more performant, and easier to understand.

Configuration Breakdown

Global Ignores

globalIgnores(['dist'])
Files and directories to ignore during linting:
  • dist: Production build output directory
These patterns apply to all configurations in the array.

File Patterns

files: ['**/*.{ts,tsx}']
This configuration applies to:
  • All .ts files (TypeScript)
  • All .tsx files (TypeScript with JSX)
JavaScript files (.js, .jsx) are not linted by default. This template is TypeScript-first.

Plugins and Presets

js.configs.recommended
ESLint’s core recommended rules for JavaScript:
  • No unused variables
  • No undefined variables
  • No unreachable code
  • Proper use of === vs ==
  • And more…

TypeScript ESLint

tseslint.configs.recommended
TypeScript-specific linting rules from typescript-eslint:
  • @typescript-eslint/no-explicit-any: Warns against using any type
  • @typescript-eslint/no-unused-vars: Detects unused variables (TypeScript-aware)
  • @typescript-eslint/no-unsafe-assignment: Prevents unsafe type assignments
  • @typescript-eslint/no-unsafe-call: Prevents calling potentially unsafe functions
  • @typescript-eslint/explicit-function-return-type: Enforces return type declarations
  • @typescript-eslint/no-non-null-assertion: Warns on ! non-null assertions
Dependencies:
  • typescript-eslint: TypeScript parser and rules
  • @typescript-eslint/parser: Parses TypeScript syntax
  • @typescript-eslint/eslint-plugin: TypeScript-specific lint rules

React Hooks Plugin

reactHooks.configs['recommended-latest']
Enforces the Rules of Hooks:
react-hooks/rules-of-hooks
error
Enforces:
  • Hooks must be called at the top level (not in loops, conditions, or nested functions)
  • Hooks must be called in React function components or custom hooks
react-hooks/exhaustive-deps
warning
Verifies the dependency array in useEffect, useMemo, useCallback, etc:
  • Warns when dependencies are missing
  • Suggests adding missing dependencies
  • Prevents stale closure bugs
The recommended-latest preset uses the most up-to-date rules for React 19+.

React Refresh Plugin

reactRefresh.configs.vite
Ensures components are compatible with Vite’s Fast Refresh (HMR):
The react-refresh/only-export-components rule enforces:Allowed exports:
// ✅ Components
export function MyComponent() { ... }
export const MyComponent = () => { ... }

// ✅ Named exports with const/let/var
export const CONSTANT = 'value'
Not allowed:
// ❌ Non-component functions mixed with components
export function MyComponent() { ... }
export function utilityFunction() { ... }  // Breaks Fast Refresh
This ensures Fast Refresh can reliably update components without full page reloads.
Files that export both components and non-component values may break Fast Refresh. Keep utilities in separate files.

Language Options

languageOptions: {
  ecmaVersion: 2020,
  globals: globals.browser,
}
ecmaVersion
number
default:"2020"
ECMAScript version to parse. 2020 includes:
  • Optional chaining (?.)
  • Nullish coalescing (??)
  • BigInt
  • Promise.allSettled
  • Dynamic import()
globals
object
Defines available global variables:
  • browser: window, document, console, fetch, etc.
  • Prevents “undefined variable” errors for browser APIs

Running ESLint

Lint Command

npm run lint
Runs ESLint on all TypeScript files:
package.json
"scripts": {
  "lint": "eslint ."
}
The . argument means “lint the current directory and all subdirectories”.

Auto-fix Issues

npm run lint -- --fix
Automatically fixes issues that ESLint can safely repair:
  • Formatting issues
  • Missing semicolons
  • Unused imports
  • Inconsistent quotes

Check Specific Files

npx eslint src/App.tsx
npx eslint "src/**/*.{ts,tsx}"

IDE Integration

VS Code

Install the ESLint extension:
code --install-extension dbaeumer.vscode-eslint
Settings:
.vscode/settings.json
{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}
This enables:
  • Real-time linting in the editor
  • Auto-fix on save
  • Inline error messages

Customizing Rules

Override Specific Rules

eslint.config.js
export default tseslint.config([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    rules: {
      // Customize rules here
      '@typescript-eslint/no-explicit-any': 'warn', // Change from error to warning
      '@typescript-eslint/no-unused-vars': ['error', { 
        argsIgnorePattern: '^_',  // Allow unused args starting with _
        varsIgnorePattern: '^_'   // Allow unused vars starting with _
      }],
      'react-hooks/exhaustive-deps': 'warn', // Downgrade to warning
    },
  },
])

Add Additional Plugins

eslint.config.js
import jsxA11y from 'eslint-plugin-jsx-a11y'

export default tseslint.config([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
      jsxA11y.flatConfigs.recommended,  // Accessibility rules
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])
After adding plugins, install them with npm install -D eslint-plugin-jsx-a11y

Disable Rules for Specific Files

eslint.config.js
export default tseslint.config([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [...],
    languageOptions: {...},
  },
  {
    files: ['**/*.test.{ts,tsx}'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',  // Allow any in tests
    },
  },
])

Ignoring Code

Ignore Specific Lines

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = externalLibrary()

Ignore Entire File

/* eslint-disable @typescript-eslint/no-explicit-any */

// Rest of file...
Use eslint-disable sparingly. It’s better to fix the underlying issue than to ignore it.

Ignore Files via Config

eslint.config.js
export default tseslint.config([
  globalIgnores(['dist', '**/*.config.{js,ts}', 'src/legacy/**']),
  // ... rest of config
])

Common Issues

”Unsafe assignment of an any value”

Problem: TypeScript ESLint detects unsafe use of any types. Solution: Provide explicit types or use type assertions:
// ❌ Bad
const data = await fetch('/api').then(r => r.json())

// ✅ Good
interface ApiResponse {
  id: number
  name: string
}

const data: ApiResponse = await fetch('/api').then(r => r.json())

“React Hook useEffect has a missing dependency”

Problem: Effect dependencies don’t match values used inside the effect. Solution: Add missing dependencies or use the suggested fix:
// ❌ Bad
useEffect(() => {
  fetchData(userId)
}, [])  // Missing userId

// ✅ Good
useEffect(() => {
  fetchData(userId)
}, [userId])  // Include userId

“Fast Refresh only works with components”

Problem: File exports non-component alongside components. Solution: Move utilities to separate files:
// ❌ Bad - App.tsx
export function App() { ... }
export function utilityFunction() { ... }

// ✅ Good - App.tsx
export function App() { ... }

// ✅ Good - utils.ts
export function utilityFunction() { ... }

Resources

Build docs developers (and LLMs) love