Production-ready React starter: React 19 + Vite, TypeScript (strict), Tailwind CSS v4, React Router, Zustand (client state) + TanStack Query (server state), Axios with interceptors, i18next, and Vitest.
Part of the Base Solution set. Angular and Vue siblings share the same architecture.
npm install
npm run dev # http://localhost:5173Sign in with demo@example.com / password → open Users.
| Script | Purpose |
|---|---|
npm run dev |
Vite dev server with HMR |
npm run build |
Type-check (tsc -b) + production build to dist/ |
npm run preview |
Serve the production build locally |
npm run lint |
oxlint |
npm run format / format:check |
Prettier write / check |
npm run typecheck |
Type-check without emitting |
npm test / test:watch / test:coverage |
Vitest |
State is split by ownership:
- Server state (users, anything fetched) → TanStack Query. Caching,
refetching, retries, and invalidation live here. See
features/users/users.queries.ts. - Client/UI state (auth session, dialogs, filters) → Zustand or local
component state. See
features/auth/auth.store.ts.
This separation is the single most important convention: don't put server data in Zustand, and don't put UI state in Query.
src/
├── app/ # App-wide wiring
│ ├── providers.tsx # StrictMode + Query + i18n providers
│ ├── query-client.ts # TanStack Query defaults (retry, staleTime)
│ └── router.tsx # Route table with lazy() code splitting
├── config/
│ └── env.ts # Typed access to import.meta.env (single source)
├── lib/
│ ├── api/
│ │ ├── http.ts # Axios instance + interceptors + ApiError
│ │ └── token-storage.ts
│ └── i18n/ # i18next init + en/vi locales
├── components/
│ ├── ui/ # Button, Input, Card, Spinner (design-token based)
│ └── layout/ # AppLayout (nav, language switch, logout)
├── features/ # Feature-first modules
│ ├── auth/ # store, api (mock), guard, LoginPage, types
│ └── users/ # api, queries, types, list page, form dialog
├── pages/ # Cross-feature pages (Home, NotFound)
├── hooks/ # Reusable hooks (useDebounce)
├── test/ # Vitest setup
└── main.tsx # Entry: providers + RouterProvider
@/ → src/ (configured in vite.config.ts and tsconfig.app.json). Import
@/features/users/... rather than long relative paths.
- Request interceptor attaches
Authorization: Bearer <token>. - Response interceptor performs a single, deduplicated token refresh on
401, replays the original request, and on failure dispatches anauth:logoutevent the auth store listens for. - Every error becomes an
ApiError(status,code,fieldErrors) — UI and queries branch on this, never on raw Axios errors.
ProtectedRoute (features/auth/ProtectedRoute.tsx) wraps private routes; it
redirects unauthenticated users to /login and restores the intended URL after
login. The mock lives in features/auth/auth.api.ts — replace it with a real
http.post('/auth/login') call (the comment shows exactly how).
Example: a Products feature.
src/features/products/products.types.ts—Product,CreateProductInput.products.api.ts— thin Axios calls using@/lib/api/http.products.queries.ts—useProducts,useCreateProduct, … with aproductKeysfactory (copy the shape fromusers.queries.ts).ProductsListPage.tsx— the UI, using@/components/ui/*.- Register a lazy route in
src/app/router.tsxunder the protected layout. - Add nav link in
components/layout/AppLayout.tsxand i18n keys inlib/i18n/locales/*.json. - Add a test next to the component (
*.test.tsx).
Vitest + Testing Library, jsdom environment, setup in src/test/setup.ts.
Example: src/components/ui/Button.test.tsx. Run npm test.
Copy .env.example → .env. Client-exposed vars must be prefixed VITE_:
| Var | Meaning |
|---|---|
VITE_APP_NAME |
Display name |
VITE_API_BASE_URL |
Backend base URL (defaults to a public mock) |
VITE_DEFAULT_LOCALE |
Fallback language |
VITE_ENABLE_MOCK |
Reserved flag for mock toggles |
.env.production sets VITE_API_BASE_URL=/api for reverse-proxy deployment.
docker build -t react-base .
docker run -p 8080:80 react-baseMulti-stage build (Node → nginx). nginx.conf has SPA fallback + asset caching.