--- name: vite-react-helper description: | Vite + React for fast modern web development - build config, HMR, hooks, state management, and performance patterns When user works with Vite, React, creates components, manages state, uses hooks, or configures Vite builds --- # Vite + React Helper Agent ## What's New (2024-2025) ### Vite - **Rolldown preview**: Rust-based bundler for faster builds - **Environment API**: Unified dev/build environment handling - **Text-based lockfile**: Better dependency tracking - **Node.js 20.19+** required ### React 19 - **React Compiler**: Automatic memoization (experimental) - **`use()` hook**: Read promises and context in render - **Actions**: Simplified async mutations - **Server Components**: First-class RSC support - **Document metadata**: Native ``, `<meta>` in components ## Project Setup ### Create New Project ```bash # Bun bun create vite my-app --template react-ts cd my-app bun install bun run dev ``` ### Templates Available | Template | Description | | -------------- | --------------------------- | | `react` | React with JavaScript | | `react-ts` | React with TypeScript | | `react-swc` | React + SWC (faster builds) | | `react-swc-ts` | React + SWC + TypeScript | ## Vite Configuration ### Basic Config ```typescript // vite.config.ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { port: 3000, open: true, }, build: { outDir: "dist", sourcemap: true, }, }); ``` ### Plugin Options ```typescript import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [ react({ // JSX runtime (automatic or classic) jsxRuntime: "automatic", // Custom JSX import source (e.g., for Emotion) jsxImportSource: "@emotion/react", // Babel plugins for features like decorators babel: { plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]], }, // File patterns to include include: /\.(mdx|js|jsx|ts|tsx)$/, }), ], }); ``` ### SWC Plugin (Faster Alternative) ```typescript import react from "@vitejs/plugin-react-swc"; export default defineConfig({ plugins: [ react({ // Enable React Refresh (default: true) devTarget: "es2022", // SWC plugins plugins: [["@swc/plugin-emotion", {}]], }), ], }); ``` ### Environment Variables ```bash # .env VITE_API_URL=https://api.example.com VITE_APP_TITLE=My App # .env.development VITE_API_URL=http://localhost:8080 # .env.production VITE_API_URL=https://api.prod.example.com ``` ```typescript // Access in code (only VITE_ prefixed vars exposed) const apiUrl = import.meta.env.VITE_API_URL; const isDev = import.meta.env.DEV; const isProd = import.meta.env.PROD; const mode = import.meta.env.MODE; ``` ```typescript // TypeScript definitions (vite-env.d.ts) /// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_URL: string; readonly VITE_APP_TITLE: string; } interface ImportMeta { readonly env: ImportMetaEnv; } ``` ### Path Aliases ```typescript // vite.config.ts import path from "path"; export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), "@components": path.resolve(__dirname, "./src/components"), "@hooks": path.resolve(__dirname, "./src/hooks"), }, }, }); ``` ```json // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"] } } } ``` ## React Core Patterns ### Components and Props ```tsx // Props with TypeScript interface ButtonProps { label: string; variant?: "primary" | "secondary"; disabled?: boolean; onClick?: () => void; children?: React.ReactNode; } function Button({ label, variant = "primary", disabled = false, onClick, children, }: ButtonProps) { return ( <button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick} > {children ?? label} </button> ); } ``` ### State with useState ```tsx import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); // Functional updates for derived state const increment = () => setCount((prev) => prev + 1); const decrement = () => setCount((prev) => prev - 1); return ( <div> <p>Count: {count}</p> <button onClick={decrement}>-</button> <button onClick={increment}>+</button> </div> ); } ``` ### Complex State with useReducer ```tsx import { useReducer } from "react"; interface State { count: number; step: number; } type Action = | { type: "increment" } | { type: "decrement" } | { type: "setStep"; step: number } | { type: "reset" }; function reducer(state: State, action: Action): State { switch (action.type) { case "increment": return { ...state, count: state.count + state.step }; case "decrement": return { ...state, count: state.count - state.step }; case "setStep": return { ...state, step: action.step }; case "reset": return { count: 0, step: 1 }; } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 }); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: "decrement" })}>-</button> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "reset" })}>Reset</button> </div> ); } ``` ### Effects and Cleanup ```tsx import { useEffect, useState } from "react"; function ChatRoom({ roomId }: { roomId: string }) { const [messages, setMessages] = useState<Message[]>([]); useEffect(() => { // Setup const connection = createConnection(roomId); connection.connect(); connection.on("message", (msg) => { setMessages((prev) => [...prev, msg]); }); // Cleanup (runs on unmount or before next effect) return () => { connection.disconnect(); }; }, [roomId]); // Re-run when roomId changes return <MessageList messages={messages} />; } ``` ### Refs for DOM Access ```tsx import { useRef, useEffect } from "react"; function VideoPlayer({ src }: { src: string }) { const videoRef = useRef<HTMLVideoElement>(null); useEffect(() => { // Direct DOM manipulation if (videoRef.current) { videoRef.current.play(); } }, [src]); return <video ref={videoRef} src={src} />; } ``` ### Context for Global State ```tsx import { createContext, useContext, useState, ReactNode } from "react"; interface ThemeContextType { theme: "light" | "dark"; toggle: () => void; } const ThemeContext = createContext<ThemeContextType | null>(null); function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<"light" | "dark">("light"); const toggle = () => { setTheme((prev) => (prev === "light" ? "dark" : "light")); }; return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> ); } function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within ThemeProvider"); } return context; } // Usage function App() { return ( <ThemeProvider> <ThemedButton /> </ThemeProvider> ); } function ThemedButton() { const { theme, toggle } = useTheme(); return <button onClick={toggle}>Current: {theme}</button>; } ``` ## Performance Optimization ### Vercel React Performance Checklist When reviewing React code, selectively apply the repo-owned adaptation in `references/vercel-react-best-practices.md`: remove request waterfalls, defer non-critical work, keep initial bundles small, avoid unnecessary re-renders, and measure before optimizing. Keep this guidance compatible with the repo's Bun/Vite toolchain and local tests; do not introduce Next.js, Vercel deployment, or Vercel-specific optimization workflows unless the project already uses them. ### useMemo for Expensive Calculations ```tsx import { useMemo } from "react"; function FilteredList({ items, filter }: Props) { // Only recalculates when items or filter changes const filteredItems = useMemo(() => { return items.filter((item) => item.name.includes(filter)); }, [items, filter]); return ( <ul> {filteredItems.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); } ``` ### useCallback for Stable References ```tsx import { useCallback, memo } from "react"; // Memoized child component const ExpensiveChild = memo(function ExpensiveChild({ onClick, }: { onClick: () => void; }) { console.log("Child rendered"); return <button onClick={onClick}>Click</button>; }); function Parent() { const [count, setCount] = useState(0); // Stable function reference const handleClick = useCallback(() => { console.log("Clicked"); }, []); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount((c) => c + 1)}>Increment</button> <ExpensiveChild onClick={handleClick} /> </div> ); } ``` ### Lazy Loading Components ```tsx import { lazy, Suspense } from "react"; // Dynamic import - only loads when needed const HeavyChart = lazy(() => import("./HeavyChart")); const AdminPanel = lazy(() => import("./AdminPanel")); function App() { const [showChart, setShowChart] = useState(false); return ( <div> <button onClick={() => setShowChart(true)}>Show Chart</button> <Suspense fallback={<div>Loading chart...</div>}> {showChart && <HeavyChart />} </Suspense> <Suspense fallback={<div>Loading admin...</div>}> <AdminPanel /> </Suspense> </div> ); } ``` ### Code Splitting Routes ```tsx import { lazy, Suspense } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; const Home = lazy(() => import("./pages/Home")); const About = lazy(() => import("./pages/About")); const Dashboard = lazy(() => import("./pages/Dashboard")); function App() { return ( <BrowserRouter> <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </Suspense> </BrowserRouter> ); } ``` ## Custom Hooks ### Data Fetching Hook ```tsx import { useState, useEffect } from "react"; interface UseFetchResult<T> { data: T | null; loading: boolean; error: Error | null; refetch: () => void; } function useFetch<T>(url: string): UseFetchResult<T> { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const fetchData = async () => { setLoading(true); setError(null); try { const response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch"); const json = await response.json(); setData(json); } catch (err) { setError(err instanceof Error ? err : new Error("Unknown error")); } finally { setLoading(false); } }; useEffect(() => { let ignore = false; (async () => { try { const response = await fetch(url); const json = await response.json(); if (!ignore) { setData(json); setLoading(false); } } catch (err) { if (!ignore) { setError(err instanceof Error ? err : new Error("Unknown error")); setLoading(false); } } })(); return () => { ignore = true; }; }, [url]); return { data, loading, error, refetch: fetchData }; } // Usage function UserProfile({ userId }: { userId: string }) { const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`); if (loading) return <Spinner />; if (error) return <Error message={error.message} />; if (!user) return null; return <div>{user.name}</div>; } ``` ### Local Storage Hook ```tsx import { useState, useEffect } from "react"; function useLocalStorage<T>(key: string, initialValue: T) { const [value, setValue] = useState<T>(() => { try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initialValue; } catch { return initialValue; } }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue] as const; } // Usage function Settings() { const [theme, setTheme] = useLocalStorage("theme", "light"); return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Toggle </button> ); } ``` ### Debounce Hook ```tsx import { useState, useEffect } from "react"; function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // Usage function SearchInput() { const [query, setQuery] = useState(""); const debouncedQuery = useDebounce(query, 300); useEffect(() => { if (debouncedQuery) { searchAPI(debouncedQuery); } }, [debouncedQuery]); return <input value={query} onChange={(e) => setQuery(e.target.value)} />; } ``` ## Build Optimization ### Chunk Splitting ```typescript // vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { // Vendor chunks "react-vendor": ["react", "react-dom"], router: ["react-router-dom"], ui: ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu"], }, }, }, }, }); ``` ### Analyze Bundle ```bash # Install analyzer npm install -D rollup-plugin-visualizer # vite.config.ts import { visualizer } from "rollup-plugin-visualizer"; export default defineConfig({ plugins: [ react(), visualizer({ open: true }), ], }); ``` ### Production Optimizations ```typescript export default defineConfig({ build: { // Target modern browsers target: "esnext", // Minification minify: "esbuild", // or "terser" for smaller bundles // CSS code splitting cssCodeSplit: true, // Source maps for debugging sourcemap: true, // Asset inlining threshold (bytes) assetsInlineLimit: 4096, // Chunk size warnings chunkSizeWarningLimit: 500, }, }); ``` ## Testing Setup ### Vitest Configuration ```typescript // vite.config.ts /// <reference types="vitest" /> import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom", setupFiles: "./src/test/setup.ts", css: true, }, }); ``` ```typescript // src/test/setup.ts import "@testing-library/jest-dom"; ``` ### Component Testing ```tsx import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import Counter from "./Counter"; describe("Counter", () => { it("renders initial count", () => { render(<Counter initialCount={5} />); expect(screen.getByText("Count: 5")).toBeInTheDocument(); }); it("increments on click", () => { render(<Counter initialCount={0} />); fireEvent.click(screen.getByRole("button", { name: "+" })); expect(screen.getByText("Count: 1")).toBeInTheDocument(); }); it("calls onChange when count changes", () => { const handleChange = vi.fn(); render(<Counter initialCount={0} onChange={handleChange} />); fireEvent.click(screen.getByRole("button", { name: "+" })); expect(handleChange).toHaveBeenCalledWith(1); }); }); ``` ## Common Patterns ### Form Handling ```tsx import { useState, FormEvent } from "react"; interface FormData { email: string; password: string; } function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) { const [formData, setFormData] = useState<FormData>({ email: "", password: "", }); const [errors, setErrors] = useState<Partial<FormData>>({}); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); setErrors((prev) => ({ ...prev, [name]: undefined })); }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); // Validation const newErrors: Partial<FormData> = {}; if (!formData.email) newErrors.email = "Email required"; if (!formData.password) newErrors.password = "Password required"; if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } onSubmit(formData); }; return ( <form onSubmit={handleSubmit}> <input name="email" type="email" value={formData.email} onChange={handleChange} /> {errors.email && <span>{errors.email}</span>} <input name="password" type="password" value={formData.password} onChange={handleChange} /> {errors.password && <span>{errors.password}</span>} <button type="submit">Login</button> </form> ); } ``` ### Error Boundaries ```tsx import { Component, ReactNode } from "react"; interface Props { children: ReactNode; fallback: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(): State { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("Error caught:", error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } // Usage function App() { return ( <ErrorBoundary fallback={<h1>Something went wrong</h1>}> <MyComponent /> </ErrorBoundary> ); } ``` ## Best Practices Summary 1. **Use TypeScript** for type safety and better DX 2. **Avoid unnecessary state** - derive values when possible 3. **Use keys correctly** - unique, stable identifiers for lists 4. **Clean up effects** - prevent memory leaks and stale closures 5. **Memoize expensive operations** with `useMemo` and `useCallback` 6. **Lazy load routes and heavy components** with `lazy()` and `Suspense` 7. **Extract reusable logic** into custom hooks 8. **Use SWC plugin** for faster development builds 9. **Configure chunk splitting** for optimal caching 10. **Test components** with React Testing Library ## When to Ask for Help - Server-side rendering (SSR) setup and hydration issues - React Server Components integration - Complex animation orchestration - State management library selection (Zustand, Jotai, Redux) - Performance profiling and optimization - Micro-frontend architectures with Vite