| Tool | Role | When to use |
|---|---|---|
| Vite | app bundler | New React/Vue/Svelte apps. Best DX, fast HMR. |
| Webpack | app bundler | Legacy projects only. Module Federation if needed. |
| esbuild | low-level | Library/CLI bundling. Used inside Vite/Bun. |
| Rollup | lib bundler | Publishing npm packages. Better tree-shaking than esbuild. |
| Bun | runtime + tools | Replace Node + npm + bundler with one binary. |
# Start a new project npm create vite@latest my-app -- --template react-ts cd my-app && npm install && npm run dev # Or with Bun (faster) bun create vite my-app --template react-ts bun dev
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { port: 3000, proxy: { '/api': 'http://localhost:8080' }}, build: { outDir: 'dist', sourcemap: true } })
# npm equivalents with pnpm npm install → pnpm install npm install pkg → pnpm add pkg npm run dev → pnpm dev npx vite → pnpm dlx vite # Workspace (monorepo) filter pnpm --filter my-app add lodash
useActionState — form actions & optimistic updatesuse(), useOptimistic(), useFormStatus()'use client' at the top only when you need interactivity or browser APIs.
<script setup> — less boilerplate than Options API$state, $derived)app/ ├── layout.tsx # Root shell, persists across routes ├── page.tsx # Route component → / ├── loading.tsx # Suspense fallback ├── error.tsx # Error boundary ('use client') ├── not-found.tsx # 404 ├── route.ts # API route (GET, POST handlers) └── dashboard/ ├── layout.tsx # Nested layout └── [id]/ └── page.tsx # Dynamic route → /dashboard/:id
// Server Component (default) — runs on server // Can: async/await, DB access, env vars (secure) // Cannot: useState, useEffect, event handlers async function Page() { const data = await fetchFromDB() return <div>{data}</div> } // Client Component — runs in browser // Can: useState, useEffect, event handlers 'use client' import { useState } from 'react' function Counter() { const [n, setN] = useState(0) return <button onClick={() => setN(n+1)}>{n}</button> }
const { data, isPending, error } = useQuery({ queryKey: ['todos', userId], queryFn: () => fetch(`/api/todos/${userId}`).then(r => r.json()) })
// 1. Infer types from Zod schemas import { z } from 'zod' const UserSchema = z.object({ id: z.string(), name: z.string().min(1), role: z.enum(['admin', 'user']) }) type User = z.infer<typeof UserSchema> // 2. Discriminated unions for component variants type Status = | { state: 'loading' } | { state: 'error'; message: string } | { state: 'success'; data: User[] } // 3. Satisfies — validate without widening type const config = { theme: 'dark', port: 3000 } satisfies Config // still infers exact type // 4. Template literal types type EventName = `on${Capitalize<string>}`
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler", // NEW
"strict": true, // always
"noUncheckedIndexedAccess": true, // catches arr[i]
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true, // NEW in 5.x
"paths": { "@/*": ["./src/*"] }
}
}
clsx + tailwind-merge for conditional classes// clsx + tailwind-merge pattern import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge' const cn = (...inputs) => twMerge(clsx(inputs))
/* Button.module.css */ .btn { padding: 8px 16px; } .primary { background: blue; } // Button.tsx import s from './Button.module.css' const Button = () => ( <button className={`${s.btn} ${s.primary}`}> Click </button> )
// vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test-setup.ts'] } }) // test file — same API as Jest import { describe, it, expect, vi } from 'vitest' describe('add', () => { it('sums numbers', () => { expect(add(1, 2)).toBe(3) }) })
import { render, screen, userEvent } from '@testing-library/react' it('increments counter', async () => { render(<Counter />) await userEvent.click(screen.getByRole('button')) expect(screen.getByText('1')).toBeInTheDocument() }) // Prefer: getByRole, getByLabelText, getByText // Avoid: getByTestId (couples tests to impl)
import { test, expect } from '@playwright/test' test('login flow', async ({ page }) => { await page.goto('/login') await page.fill('[name=email]', 'user@test.com') await page.click('button[type=submit]') await expect(page).toHaveURL('/dashboard') })
The biggest paradigm shift in React since hooks. Components run on the server, stream HTML to the client, and only the interactive parts ship JS. Fundamentally changes how you think about data fetching and component boundaries.
Astro popularized this: render everything as static HTML, then hydrate only interactive "islands" independently. RSC achieves a similar result within React. Key insight: most content doesn't need JS.
Run server code at CDN edge nodes globally instead of a single region. Cloudflare Workers, Vercel Edge Functions, Deno Deploy. Subset of Node.js APIs available — web standards only (fetch, Request, Response, etc.).
Streaming LLM responses via ReadableStream, Vercel AI SDK for React hooks around streaming, generative UI (server streams React components to client). Entire new category of UI patterns emerging.