Setting up a React project with Vite
Scaffold with create-vite, understand the folder layout, how Fast Refresh and the automatic JSX transform work, and the TypeScript and lint defaults.
Scaffold and layout
// src/main.tsx - the single entry point of the application
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)| Path | What lives there |
|---|---|
index.html | The single HTML shell; Vite injects the bundle here |
src/main.tsx | Mounts the app; the only file that touches the DOM directly |
src/App.tsx | The root component; real apps mount the router here |
src/components/ | Reusable components, usually one folder each with its styles and tests |
src/hooks/ | Custom hooks shared across features |
src/assets/ | Images and fonts imported by components so the bundler fingerprints them |
public/ | Files copied as-is; referenced by absolute path, never imported |
Fast Refresh and the JSX transform
// no React import is needed: the automatic JSX runtime handles it
function Panel({ title, children }) {
return (
<section className="panel">
<h2>{title}</h2>
{children}
</section>
)
}
// editing this file updates the panel in place, keeping its state
export default function App() {
return (
<Panel title="Release notes">
<p>Fast Refresh replaced this component without reloading the page.</p>
</Panel>
)
}- The automatic runtime is what removes
import React from 'react'; the build targets a modern JSX transform, so do not add the old import back. - Fast Refresh preserves component state for edits in the module graph. Editing a file that only exports non-components (a constant, a helper) triggers a full page reload instead.
- Module scope is preserved across refreshes, so a module-level mutable value goes stale in ways component state does not - keep mutable state inside components or a store.
- Vite serves native ES modules in development, which is why startup is fast and why
importpaths must include the extension where the resolver requires it. - The dev server never type-checks. Add
tsc --noEmitto CI, or use the defaultbuildscript, which runs the checker before bundling.
TypeScript, lint and formatting
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: { port: 5173, open: true },
build: { sourcemap: true },
resolve: { alias: { '@': new URL('./src', import.meta.url).pathname } }
})- The
react-tstemplate writestsconfig.app.jsonfor application code withstrictandjsx: "react-jsx", andtsconfig.node.jsonfor the config files that run in Node. - Set
noUnusedLocalsandnoUnusedParametersfrom the start: unused imports accumulate quickly in component files. eslint-plugin-react-hooksis what catches a missing effect dependency and a conditional hook call. Keep it in the pipeline rather than fixing those by review.- Run the formatter on save and in a pre-commit hook, so formatting never appears in a functional diff.
- Add
npm run buildto CI; it runs the type check and the production bundle together.
⚠️
A green
vite build only proves the code bundles, because the tooling strips types without checking them. Run tsc --noEmit as its own step, otherwise a type error can reach production through a passing pipeline.FAQ
Why does the template create two tsconfig files?
Application code needs the DOM libraries and
jsx: react-jsx, while vite.config.ts runs in Node with different globals and no DOM. One file cannot describe both without weakening the checks.When should I use Next.js instead of Vite?
When you need server rendering, file-based routing or server components. Vite is a client-side build tool: it is lighter, faster to start, and the better default for a dashboard or an internal tool behind a login.
Related
Production builds, environment config and deployment Testing React components
Last refreshed 2026-09-18.