
Setting up a modern React project requires more than just scaffolding a boilerplate; it demands a robust, enforceable foundation for code quality and consistency. Vite has cemented its place as the premier build tool for modern web applications, offering unparallelled development speeds and an intuitive configuration system. However, integrating it with the evolving landscape of code quality tools - specifically, ESLint's new Flat Config system and strict TypeScript compilation - can present challenges even for seasoned developers.
In this comprehensive guide, we'll architect a highly strict, modern linting and formatting pipeline using Vite, ESLint's Flat Config (eslint.config.js), Prettier, and TypeScript. This approach abandons the legacy .eslintrc formats in favor of the unified and modular Flat Config, ensuring your configuration is future-proof and performant.
Historically, ESLint configurations relied on a cascading resolution system using .eslintrc files, which often led to confusion when multiple configuration files applied to the same directory. The Flat Config system entirely overhauls this by utilizing a single eslint.config.js file (or .mjs/.cjs) at the root of your project. This system evaluates an array of configuration objects sequentially, providing a deterministic and programmatic approach to defining rules.
The Flat Config system eliminates the extends and plugins resolution complexities, replacing them with standard JavaScript imports. This paradigm shift means we construct our linting rules as composing standard JavaScript objects, which provides type safety and clearer lineage of rule definitions.
First, let's bootstrap a new Vite project with the React and TypeScript template.
npm create vite@latest modern-react-app -- --template react-ts
cd modern-react-app
npm install
With the foundation laid, we must install the necessary dependencies for our linting and formatting pipeline. Because we are targeting the modern Flat Config, we need the latest versions of ESLint and the associated TypeScript and React plugins.
npm install -D eslint prettier eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-refresh @eslint/js typescript-eslint globals
Create an eslint.config.js file at the root of your project. This file will export an array of configuration objects. We will leverage typescript-eslint to bridge the gap between ESLint and TypeScript's AST, allowing us to enforce type-aware linting rules.
import js from '@eslint/js';
import globals from 'globals';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';
export default tseslint.config(
// Base Ignore Configuration
{ ignores: ['dist', 'node_modules', 'coverage', 'vite.config.ts'] },
// Base JavaScript rules
js.configs.recommended,
// TypeScript Configuration (Strict + Type Checked)
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.app.json', './tsconfig.node.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
// React and Environment Configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2021,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactPlugin.configs['jsx-runtime'].rules,
...reactHooksPlugin.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// Strict React Rules
'react/prop-types': 'off', // Rely on TypeScript instead
'react/no-array-index-key': 'error', // Prevent subtle rendering bugs
'react/jsx-pascal-case': 'error',
'react/jsx-no-useless-fragment': 'warn',
},
},
// Prettier Integration (Must be last to override formatting rules)
prettierConfig
);
tseslint.configs.strictTypeChecked, we enable rules that utilize TypeScript's type checker. This requires specifying the project property in parserOptions, pointing to our TypeScript configurations. This setup catches deep architectural flaws, such as unsafe any types, floating promises, and incorrect template literal usages.reactPlugin.configs['jsx-runtime'].rules ensures ESLint understands the modern JSX transform, preventing false positives for missing React imports.react-refresh/only-export-components rule enforces that modules containing components only export components, ensuring Fast Refresh operates reliably.eslint-config-prettier plugin is appended at the very end of the array. The Flat Config system evaluates objects sequentially, meaning the last object takes precedence. This effectively disables any ESLint rules that might conflict with Prettier's formatting opinions.Prettier requires minimal configuration, but standardizing it ensures consistency across development environments. Create a .prettierrc file in the root directory:
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 90,
"tabWidth": 2,
"endOfLine": "lf",
"jsxSingleQuote": false,
"arrowParens": "always"
}
Additionally, define a .prettierignore file to exclude generated assets and configuration files:
dist
coverage
node_modules
*.html
vite.config.ts
To maximize the benefits of type-aware linting, your TypeScript configuration must be equally rigorous. Modify your tsconfig.app.json (as standardized by recent Vite templates) to enforce strict type checking:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Strict Type-Checking Options */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUncheckedIndexedAccess": true, // Highly recommended for array access safety
"exactOptionalPropertyTypes": true
},
"include": ["src"]
}
The addition of noUncheckedIndexedAccess is critical. It forces developers to handle undefined cases when accessing array or record elements by index, bridging a common gap between TypeScript's static analysis and runtime reality.
While IDE integration handles inline linting, integrating ESLint directly into the Vite build process ensures that CI/CD pipelines and production builds fail if linting errors persist.
To achieve this, we can utilize vite-plugin-eslint. However, with the migration to Flat Config, you must ensure the plugin or a custom rollup plugin supports ESLint 9+ and the Flat Config API. Alternatively, executing linting as a pre-build step is a highly robust approach.
Update your package.json scripts:
{
"scripts": {
"dev": "vite",
"build": "npm run typecheck && npm run lint && vite build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"",
"typecheck": "tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json",
"preview": "vite preview"
}
}
This sequence guarantees that npm run build will perform type checking and linting before attempting to bundle the application. If any strict type rules or linting policies are violated, the build aborts.
A developer is integrating a new ESLint Flat Config (`eslint.config.js`) into an existing large-scale React project that previously used `.eslintrc.js` files. They notice that some linting rules are not being applied as expected, particularly in subdirectories that had their own `.eslintrc.js` configurations. What is the most likely reason for this behavior?
Migrating to ESLint's Flat Config alongside Vite and Prettier establishes a definitive standard for modern React development. By leaning heavily into type-aware linting through typescript-eslint and enforcing strict TypeScript compiler options, teams can proactively eliminate entire classes of runtime errors. This configuration provides a deterministic, highly performant, and future-proof foundation, allowing developers to focus on architectural complexity rather than debating code style or chasing subtle typing bugs.
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Comprehensive comparison of TanStack Query (React Query) and SWR: caching strategies, optimistic updates, mutation lifecycle, and React 19 compatibility.
Read more
Mastering React 19 form actions: instant UI updates with useOptimistic, server state management with useActionState, and graceful error boundaries.
Read more
Eliminate unnecessary re-renders with stable ref caching, batched event listeners, memoized selectors, and React Profiler-guided hook optimization. Copy-paste patterns for production React 18/19 apps.
Read more