# Best Practices for Development This skill provides production-ready best practices for building SPA React applications. Use this guidance when implementing features, reviewing code, or making architectural decisions. ## Stack Overview - **React 19** with React Compiler (auto-memoization) - **TypeScript** (strict mode) - **Vite** (bundler) - **Biome** (formatting + linting) - **TanStack Query** (server state) - **TanStack Router** (file-based routing) - **Vitest** (testing with jsdom) - **Apidog MCP** (API spec source of truth) ## Project Structure ``` /src /app/ # App shell, providers, global styles /routes/ # TanStack Router file-based routes /components/ # Reusable, pure UI components (no data-fetch) /features/ # Feature folders (UI + hooks local to a feature) /api/ # Generated API types & client (from OpenAPI) /lib/ # Utilities (zod schemas, date, formatting, etc.) /test/ # Test utilities ``` **Key Principles:** - One responsibility per file - UI components don't fetch server data - Put queries/mutations in feature hooks - Co-locate tests next to files ## Tooling Configuration ### 1. Vite + React 19 + React Compiler ```typescript // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [ react({ babel: { // React Compiler must run first: plugins: ['babel-plugin-react-compiler'], }, }), ], }) ``` **Verify:** Check DevTools for "Memo ✨" badge on optimized components. ### 2. TypeScript (strict + bundler mode) ```json // tsconfig.json { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "verbatimModuleSyntax": true, "isolatedModules": true, "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, "types": ["vite/client", "vitest"] }, "include": ["src", "vitest-setup.ts"] } ``` ### 3. Biome (formatter + linter) ```bash npx @biomejs/biome init npx @biomejs/biome check --write . ``` ```json // biome.json { "formatter": { "enabled": true, "lineWidth": 100 }, "linter": { "enabled": true, "rules": { "style": { "noUnusedVariables": "error" } } } } ``` ### 4. Environment Variables - Read via `import.meta.env` - Prefix all app-exposed vars with `VITE_` - Never place secrets in the client bundle ## Testing Setup (Vitest) ```typescript // vitest-setup.ts import '@testing-library/jest-dom/vitest' // vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./vitest-setup.ts'], coverage: { reporter: ['text', 'html'] } } }) ``` - Use React Testing Library for DOM assertions - Use msw for API mocks - Add `types: ["vitest", "vitest/jsdom"]` for jsdom globals ## React 19 Guidelines ### Compiler-Friendly Code - Keep components pure and props serializable - Derive values during render (don't stash in refs unnecessarily) - Keep event handlers inline unless they close over large mutable objects - Verify compiler is working (DevTools ✨) - Opt-out problematic components with `"use no memo"` while refactoring ### Actions & Forms For SPA mutations, choose one per feature: - **React 19 Actions:** `
`, `useActionState`, `useOptimistic` - **TanStack Query:** `useMutation` Don't duplicate logic between both approaches. ### `use` Hook - Primarily useful with Suspense/data primitives and RSC - For SPA-only apps, prefer Query + Router loaders ## Routing (TanStack Router) ### Installation ```bash pnpm add @tanstack/react-router pnpm add -D @tanstack/router-plugin ``` ```typescript // vite.config.ts import { TanStackRouterVite } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [react(), TanStackRouterVite()], }) ``` ### Bootstrap ```typescript // src/main.tsx import { StrictMode } from 'react' import ReactDOM from 'react-dom/client' import { RouterProvider, createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } ReactDOM.createRoot(document.getElementById('root')!).render( ) ``` ### File-Based Routes ``` src/routes/__root.tsx // layout (Outlet, providers) src/routes/index.tsx // "/" src/routes/users/index.tsx // "/users" src/routes/users/$id.tsx // "/users/:id" ``` - Supports typed search params (JSON-first) - Validate at route boundaries ## Server State (TanStack Query v5) **TanStack Query v5** (October 2023) is the async state manager for this project. It requires React 18+, features first-class Suspense support, improved TypeScript inference, and a 20% smaller bundle. This section covers production-ready patterns based on official documentation and community best practices. ### Breaking Changes in v5 **Key updates you need to know:** 1. **Single Object Signature**: All hooks now accept one configuration object: ```typescript // ✅ v5 - single object useQuery({ queryKey, queryFn, ...options }) // ❌ v4 - multiple overloads (deprecated) useQuery(queryKey, queryFn, options) ``` 2. **Renamed Options**: - `cacheTime` → `gcTime` (garbage collection time) - `keepPreviousData` → `placeholderData: keepPreviousData` - `isLoading` now means `isPending && isFetching` 3. **Callbacks Removed from useQuery**: - `onSuccess`, `onError`, `onSettled` removed from `useQuery` - Use global QueryCache callbacks instead - Prevents duplicate executions 4. **Infinite Queries Require initialPageParam**: - No default value provided - Must explicitly set `initialPageParam` (e.g., `0` or `null`) 5. **First-Class Suspense**: - New dedicated hooks: `useSuspenseQuery`, `useSuspenseInfiniteQuery` - No experimental flag needed - Data is never undefined at type level **Migration**: Use the official codemod for automatic migration: `npx @tanstack/query-codemods v5/replace-import-specifier` ### Smart Defaults Query v5 ships with production-ready defaults: ```typescript { staleTime: 0, // Data instantly stale (refetch on mount) gcTime: 5 * 60_000, // Keep unused cache for 5 minutes retry: 3, // 3 retries with exponential backoff refetchOnWindowFocus: true,// Refetch when user returns to tab refetchOnReconnect: true, // Refetch when network reconnects } ``` **Philosophy**: React Query is an **async state manager, not a data fetcher**. You provide the Promise; Query manages caching, background updates, and synchronization. ### Client Setup ```typescript // src/app/providers.tsx import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query' import { toast } from './toast' // Your notification system const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 0, // Adjust per-query gcTime: 5 * 60_000, // 5 minutes (v5: formerly cacheTime) retry: (failureCount, error) => { // Don't retry on 401 (authentication errors) if (error?.response?.status === 401) return false return failureCount < 3 }, }, }, queryCache: new QueryCache({ onError: (error, query) => { // Only show toast for background errors (when data exists) if (query.state.data !== undefined) { toast.error(`Something went wrong: ${error.message}`) } }, }), }) export function AppProviders({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` **DevTools Setup** (auto-excluded in production): ```typescript import { ReactQueryDevtools } from '@tanstack/react-query-devtools' {children} ``` ### Architecture: Feature-Based Colocation **Recommended pattern**: Group queries with related features, not by file type. ``` src/features/ ├── Todos/ │ ├── index.tsx # Feature entry point │ ├── queries.ts # All React Query logic (keys, functions, hooks) │ ├── types.ts # TypeScript types │ └── components/ # Feature-specific components ``` **Export only custom hooks** from query files. Keep query functions and keys private: ```typescript // features/todos/queries.ts // 1. Query Key Factory (hierarchical structure) const todoKeys = { all: ['todos'] as const, lists: () => [...todoKeys.all, 'list'] as const, list: (filters: string) => [...todoKeys.lists(), { filters }] as const, details: () => [...todoKeys.all, 'detail'] as const, detail: (id: number) => [...todoKeys.details(), id] as const, } // 2. Query Function (private) const fetchTodos = async (filters: string): Promise => { const response = await axios.get('/api/todos', { params: { filters } }) return response.data } // 3. Custom Hook (public API) export const useTodosQuery = (filters: string) => { return useQuery({ queryKey: todoKeys.list(filters), queryFn: () => fetchTodos(filters), staleTime: 30_000, // Fresh for 30 seconds }) } ``` **Benefits**: - Prevents key/function mismatches - Clean public API - Encapsulation and maintainability - Easy to locate all query logic for a feature ### Query Key Factories (Essential) **Structure keys hierarchically** from generic to specific: ```typescript // ✅ Correct hierarchy ['todos'] // Invalidates everything ['todos', 'list'] // Invalidates all lists ['todos', 'list', { filters }] // Invalidates specific list ['todos', 'detail', 1] // Invalidates specific detail // ❌ Wrong - flat structure ['todos-list-active'] // Can't partially invalidate ``` **Critical rule**: Query keys must include **ALL variables used in queryFn**. Treat query keys like dependency arrays: ```typescript // ✅ Correct - includes all variables const { data } = useQuery({ queryKey: ['todos', filters, sortBy], queryFn: () => fetchTodos(filters, sortBy), }) // ❌ Wrong - missing variables const { data } = useQuery({ queryKey: ['todos'], queryFn: () => fetchTodos(filters, sortBy), // filters/sortBy not in key! }) ``` **Type consistency matters**: `['todos', '1']` and `['todos', 1]` are **different keys**. Be consistent with types. ### Query Options API (Type Safety) **The modern pattern** for maximum type safety across your codebase: ```typescript import { queryOptions } from '@tanstack/react-query' function todoOptions(id: number) { return queryOptions({ queryKey: ['todos', id], queryFn: () => fetchTodo(id), staleTime: 5000, }) } // ✅ Use everywhere with full type safety useQuery(todoOptions(1)) queryClient.prefetchQuery(todoOptions(5)) queryClient.setQueryData(todoOptions(42).queryKey, newTodo) queryClient.getQueryData(todoOptions(42).queryKey) // Fully typed! ``` **Benefits**: - Single source of truth for query configuration - Full TypeScript inference for imperatively accessed data - Reusable across hooks and imperative methods - Prevents key/function mismatches ### Data Transformation Strategies Choose the right approach based on your use case: **1. Transform in queryFn** - Simple cases where cache should store transformed data: ```typescript const fetchTodos = async (): Promise => { const response = await axios.get('/api/todos') return response.data.map(todo => ({ ...todo, name: todo.name.toUpperCase() })) } ``` **2. Transform with `select` option (RECOMMENDED)** - Enables partial subscriptions: ```typescript // Only re-renders when filtered data changes export const useTodosQuery = (filters: string) => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(todo => todo.status === filters), }) // Only re-renders when count changes export const useTodosCount = () => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.length, }) ``` **⚠️ Memoize select functions** to prevent running on every render: ```typescript // ✅ Stable reference const transformTodos = (data: Todo[]) => expensiveTransform(data) const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: transformTodos, // Stable function reference }) // ❌ Runs on every render const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => expensiveTransform(data), // New function every render }) ``` ### TypeScript Best Practices **Let TypeScript infer types** from queryFn rather than specifying generics: ```typescript // ✅ Recommended - inference const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, // Returns Promise }) // data is Todo[] | undefined // ❌ Unnecessary - explicit generics const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) ``` **Discriminated unions** automatically narrow types: ```typescript const { data, isSuccess, isError, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) if (isSuccess) { // data is Todo[] (never undefined) } if (isError) { // error is defined } ``` Use `queryOptions` helper for maximum type safety across imperative methods. ### Custom Hooks Pattern **Always create custom hooks** even for single queries: ```typescript // ✅ Recommended - custom hook with encapsulation export function usePost( id: number, options?: Omit, 'queryKey' | 'queryFn'> ) { return useQuery({ queryKey: ['posts', id], queryFn: () => getPost(id), ...options, }) } // Usage: allows callers to override any option except key/fn const { data } = usePost(42, { staleTime: 10_000 }) ``` **Benefits**: - Centralizes query logic - Easy to update all usages - Consistent configuration - Better testing ### Error Handling (Multi-Layer Strategy) **Layer 1: Component-Level** - Specific user feedback: ```typescript function TodoList() { const { data, error, isError, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) if (isLoading) return if (isError) return {error.message} return
    {data.map(todo => )}
} ``` **Layer 2: Global Error Handling** - Background errors via QueryCache: ```typescript // Already configured in client setup above queryCache: new QueryCache({ onError: (error, query) => { if (query.state.data !== undefined) { toast.error(`Background error: ${error.message}`) } }, }) ``` **Layer 3: Error Boundaries** - Catch render errors: ```typescript import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' {({ reset }) => ( (

Error: {error.message}

)} >
)}
``` ### Suspense Integration **First-class Suspense support** in v5 with dedicated hooks: ```typescript import { useSuspenseQuery } from '@tanstack/react-query' function TodoList() { // data is NEVER undefined (type-safe) const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return
    {data.map(todo => )}
} // Wrap with Suspense boundary function App() { return ( }> ) } ``` **Benefits**: - Eliminates loading state management - Data always defined (TypeScript enforced) - Cleaner component code - Works with React.lazy for code-splitting ### Mutations with Optimistic Updates **Basic mutation** with cache invalidation: ```typescript export function useCreateTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: (newTodo: CreateTodoDTO) => api.post('/todos', newTodo).then(res => res.data), onSuccess: (data) => { // Set detail query immediately queryClient.setQueryData(['todos', data.id], data) // Invalidate list queries queryClient.invalidateQueries({ queryKey: ['todos', 'list'] }) }, }) } ``` **Simple optimistic updates** using `variables`: ```typescript const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/todos', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, variables, mutate } = addTodoMutation return (
    {todoQuery.data?.map(todo =>
  • {todo.text}
  • )} {isPending &&
  • {variables}
  • }
) ``` **Advanced optimistic updates** with rollback: ```typescript useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { // Cancel outgoing queries (prevent race conditions) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot current data const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update cache queryClient.setQueryData(['todos'], (old: Todo[]) => old?.map(todo => todo.id === newTodo.id ? newTodo : todo) ) // Return context for rollback return { previousTodos } }, onError: (err, newTodo, context) => { // Rollback on error queryClient.setQueryData(['todos'], context?.previousTodos) toast.error('Update failed. Changes reverted.') }, onSettled: () => { // Always refetch to ensure consistency queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) ``` **Key principles**: - Cancel ongoing queries in `onMutate` to prevent race conditions - Snapshot previous data before updating - Restore snapshot on error - Always invalidate in `onSettled` for eventual consistency - **Never mutate cached data directly** - always use immutable updates ### Authentication Integration **Handle token refresh at HTTP client level** (not React Query): ```typescript // src/lib/api-client.ts import axios from 'axios' import createAuthRefreshInterceptor from 'axios-auth-refresh' export const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_URL, }) // Add token to requests apiClient.interceptors.request.use((config) => { const token = getAccessToken() if (token) config.headers.Authorization = `Bearer ${token}` return config }) // Refresh token on 401 const refreshAuth = async (failedRequest: any) => { try { const newToken = await fetchNewToken() failedRequest.response.config.headers.Authorization = `Bearer ${newToken}` setAccessToken(newToken) return Promise.resolve() } catch { removeAccessToken() window.location.href = '/login' return Promise.reject() } } createAuthRefreshInterceptor(apiClient, refreshAuth, { statusCodes: [401], pauseInstanceWhileRefreshing: true, }) ``` **Protected queries** use the `enabled` option: ```typescript const useTodos = () => { const { user } = useUser() // Get current user from auth context return useQuery({ queryKey: ['todos', user?.id], queryFn: () => fetchTodos(user.id), enabled: !!user, // Only execute when user exists }) } ``` **On logout**: Clear the entire cache with `queryClient.clear()` (not `invalidateQueries()` which triggers refetches): ```typescript const logout = () => { removeAccessToken() queryClient.clear() // Clear all cached data navigate('/login') } ``` ### Advanced Patterns **Prefetching** - Eliminate loading states: ```typescript // Hover prefetching function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, staleTime: 60_000, // Consider fresh for 1 minute }) } return ( ) } // Route-level prefetching (see Router × Query Integration section) ``` **Infinite Queries** - Infinite scrolling/pagination: ```typescript function Projects() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, // Required in v5 getNextPageParam: (lastPage) => lastPage.nextCursor, }) if (isLoading) return return ( <> {data.pages.map((page, i) => ( {page.data.map(project => ( ))} ))} ) } ``` **Offset-Based Pagination** with `placeholderData`: ```typescript import { keepPreviousData } from '@tanstack/react-query' function Posts() { const [page, setPage] = useState(0) const { data, isPending, isPlaceholderData } = useQuery({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), placeholderData: keepPreviousData, // Show previous data while fetching }) return ( <> {data.posts.map(post => )} ) } ``` **Dependent Queries** - Sequential data fetching: ```typescript function UserProjects({ email }: { email: string }) { // First query const { data: user } = useQuery({ queryKey: ['user', email], queryFn: () => getUserByEmail(email), }) // Second query waits for first const { data: projects } = useQuery({ queryKey: ['projects', user?.id], queryFn: () => getProjectsByUser(user.id), enabled: !!user?.id, // Only runs when user.id exists }) return
{/* render projects */}
} ``` ### Performance Optimization **staleTime is your primary control** - adjust this, not `gcTime`: ```typescript // Real-time data (default) staleTime: 0 // Always considered stale, refetch on mount // User profiles (changes infrequently) staleTime: 1000 * 60 * 2 // Fresh for 2 minutes // Static reference data staleTime: 1000 * 60 * 10 // Fresh for 10 minutes ``` **Query deduplication** happens automatically - multiple components mounting with identical query keys result in a single network request, but all components receive data. **Prevent request waterfalls**: ```typescript // ❌ Waterfall - each query waits for previous function Dashboard() { const { data: user } = useQuery(userQuery) const { data: posts } = useQuery(postsQuery(user?.id)) const { data: stats } = useQuery(statsQuery(user?.id)) } // ✅ Parallel - all queries start simultaneously function Dashboard() { const { data: user } = useQuery(userQuery) const { data: posts } = useQuery({ ...postsQuery(user?.id), enabled: !!user?.id, }) const { data: stats } = useQuery({ ...statsQuery(user?.id), enabled: !!user?.id, }) } // ✅ Best - prefetch in route loader (see Router × Query Integration) ``` **Never copy server state to local state** - this opts out of background updates: ```typescript // ❌ Wrong - copies to state, loses reactivity const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) const [todos, setTodos] = useState(data) // ✅ Correct - use query data directly const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) ``` ### Testing with Mock Service Worker (MSW) **MSW is the recommended approach** - mock the network layer: ```typescript // src/test/mocks/handlers.ts import { http, HttpResponse } from 'msw' export const handlers = [ http.get('/api/todos', () => { return HttpResponse.json([ { id: 1, text: 'Test todo', completed: false }, ]) }), http.post('/api/todos', async ({ request }) => { const newTodo = await request.json() return HttpResponse.json({ id: 2, ...newTodo }) }), ] // src/test/setup.ts import { setupServer } from 'msw/node' import { handlers } from './mocks/handlers' export const server = setupServer(...handlers) beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) ``` **Create test wrappers** with proper QueryClient: ```typescript // src/test/utils.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render } from '@testing-library/react' export function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, // Prevent retries in tests gcTime: Infinity, }, }, }) } export function renderWithClient(ui: React.ReactElement) { const testQueryClient = createTestQueryClient() return render( {ui} ) } ``` **Test queries**: ```typescript import { renderWithClient } from '@/test/utils' import { screen } from '@testing-library/react' test('displays todos', async () => { renderWithClient() // Wait for data to load expect(await screen.findByText('Test todo')).toBeInTheDocument() }) test('shows error state', async () => { // Override handler for this test server.use( http.get('/api/todos', () => { return HttpResponse.json( { message: 'Failed to fetch' }, { status: 500 } ) }) ) renderWithClient() expect(await screen.findByText(/failed/i)).toBeInTheDocument() }) ``` **Critical testing principles**: - Create new QueryClient per test for isolation - Set `retry: false` to prevent timeouts - Use async queries (`findBy*`) for data that loads - Silence console.error for expected errors ### Anti-Patterns to Avoid **❌ Don't store query data in Redux/Context**: - Creates dual sources of truth - Loses automatic cache invalidation - Triggers unnecessary renders **❌ Don't call refetch() with different parameters**: ```typescript // ❌ Wrong - breaks declarative pattern const { data, refetch } = useQuery({ queryKey: ['todos'], queryFn: () => fetchTodos(filters), }) // Later: refetch with different filters??? Won't work! // ✅ Correct - include params in key const [filters, setFilters] = useState('all') const { data } = useQuery({ queryKey: ['todos', filters], queryFn: () => fetchTodos(filters), }) // Changing filters automatically refetches ``` **❌ Don't use queries for local state**: - Query Cache expects refetchable data - Use useState/useReducer for client-only state **❌ Don't create QueryClient inside components**: ```typescript // ❌ Wrong - new cache every render function App() { const client = new QueryClient() return ... } // ✅ Correct - stable instance const queryClient = new QueryClient() function App() { return ... } ``` **❌ Don't ignore loading and error states** - always handle both **❌ Don't transform data by copying to state** - use `select` option **❌ Don't mismatch query keys** - be consistent with types (`'1'` vs `1`) ### Cache Timing Guidelines **staleTime** - How long data is considered fresh: - `0` (default) - Always stale, refetch on mount/focus - `30_000` (30s) - Good for user-generated content - `120_000` (2min) - Good for profile data - `600_000` (10min) - Good for static reference data **gcTime** (formerly cacheTime) - How long unused data stays in cache: - `300_000` (5min, default) - Good for most cases - `Infinity` - Keep forever (useful with persistence) - `0` - Immediate garbage collection (not recommended) **Relationship**: `staleTime` controls refetch frequency, `gcTime` controls memory cleanup. ## Router × Query Integration ### Route Loader + Query Prefetch ```typescript // src/routes/users/$id.tsx import { createFileRoute } from '@tanstack/react-router' import { queryClient } from '@/app/queryClient' import { usersKeys, fetchUser } from '@/features/users/queries' export const Route = createFileRoute('/users/$id')({ loader: async ({ params }) => { const id = params.id return queryClient.ensureQueryData({ queryKey: usersKeys.detail(id), queryFn: () => fetchUser(id), staleTime: 30_000, }) }, component: UserPage, }) ``` **Benefits:** - Loaders run before render, eliminating waterfall - Fast SPA navigations - Add Router & Query DevTools during development (auto-hide in production) ## API Integration (Apidog + MCP) ### Goal The AI agent always uses the latest API. ### Process 1. **Expose OpenAPI from Apidog** (3.0/3.1 export or URL) 2. **Wire MCP:** ```json // mcp.json { "mcpServers": { "API specification": { "command": "npx", "args": ["-y", "apidog-mcp-server@latest", "--oas=https://your.domain/openapi.json"] } } } ``` - Supports remote URL or local file - Multiple specs via multiple MCP entries 3. **Generate Types & Client** in `/src/api`: - Lightweight: hand-rolled fetch with zod parsing - Codegen: OpenAPI TS generator + import client 4. **Query Layer:** - Build `queryOptions`/mutation wrappers - Accept typed params, return parsed data - Keep all HTTP details under `/api` ## Performance, Accessibility, Security ### Performance - **Code-splitting:** TanStack Router file-based routing + Vite `dynamic import()` - **React Compiler first:** Keep components pure for auto-memoization - **Images & assets:** Use Vite asset pipeline; prefer modern formats ### Accessibility - Use semantic elements - Test with RTL queries (by role/label) ### Security - Never ship secrets - Only `VITE_*` envs are exposed - Validate all untrusted data at boundaries (server → zod parse) - Pin/renovate deps; avoid known compromised packages - Run CI with `--ignore-scripts` when possible ## Agent Execution Rules **Always do this when you add or modify code:** 1. **API Spec:** Fetch latest via Apidog MCP and regenerate `/src/api` types if changed 2. **Data Access:** Wire only through feature hooks that wrap TanStack Query. Never fetch inside UI components. 3. **New Routes:** - Create file under `/src/routes/**` (file-based routing) - If needs data at navigation, add loader that prefetches with Query 4. **Server Mutations:** - Use React 19 Actions OR TanStack Query `useMutation` (choose one per feature) - Use optimistic UI via `useOptimistic` (Actions) or Query's optimistic updates - Invalidate/selectively update cache on success 5. **Compiler-Friendly:** - Keep code pure (pure components, minimal effects) - If compiler flags something, fix it or add `"use no memo"` temporarily 6. **Tests:** - Add Vitest tests for new logic - Component tests use RTL - Stub network with msw 7. **Before Committing:** - Run `biome check --write` - Ensure Vite build passes ## "Done" Checklist per PR - [ ] Route file added/updated; loader prefetch (if needed) present - [ ] Query keys are stable (`as const`), `staleTime`/`gcTime` tuned - [ ] Component remains pure; no unnecessary effects; compiler ✨ visible - [ ] API calls typed from `/src/api`; inputs/outputs validated at boundaries - [ ] Tests cover new logic; Vitest jsdom setup passes - [ ] `biome check --write` clean; Vite build ok ## Authoritative Sources - **React 19 & Compiler:** - React v19 overview - React Compiler: overview + installation + verification - `` / Actions API; `useOptimistic`; `use` - CRA deprecation & guidance - **Vite:** - Getting started; env & modes; TypeScript targets - **TypeScript:** - `moduleResolution: "bundler"` (for bundlers like Vite) - **Biome:** - Formatter/Linter configuration & CLI usage - **TanStack Query:** - Caching & important defaults; v5 migration notes; devtools/persisting cache - **TanStack Router:** - Install with Vite plugin; file-based routing; search params; devtools - **Vitest:** - Getting started & config (jsdom) - **Apidog + MCP:** - Apidog docs (import/export, OpenAPI); MCP server usage ## Final Notes - Favor compile-friendly React patterns - Let the compiler and Query/Router handle perf and data orchestration - Treat Apidog's OpenAPI (via MCP) as the single source of truth for network shapes - Keep this doc as your "contract"—don't add heavy frameworks or configs beyond what's here unless explicitly requested