--- name: react-web description: React web development with hooks, React Query, Zustand --- # React Web Skill *Load with: base.md + typescript.md* --- ## Test-First Development (MANDATORY) **CRITICAL: Tests MUST be written BEFORE implementation code. This is non-negotiable for frontend components.** ### The TFD Workflow ``` 1. Write test file first → Defines expected behavior 2. Run test (it fails) → Confirms test is valid 3. Write minimal code → Just enough to pass 4. Run test (it passes) → Validates implementation 5. Refactor if needed → Tests catch regressions ``` ### Component Development Order ```bash # CORRECT ORDER - Test first 1. Create Button.test.tsx # Write tests for expected behavior 2. Run tests (they fail) # npm test -- Button 3. Create Button.tsx # Implement to pass tests 4. Run tests (they pass) # Verify implementation 5. Create Button.module.css # Style after logic works # WRONG ORDER - Never do this 1. Create Button.tsx # ❌ No tests exist yet 2. Create Button.module.css # ❌ Still no tests 3. "I'll add tests later" # ❌ Tests never get written ``` ### Test File Structure (Create First) ```typescript // Button.test.tsx - CREATE THIS FIRST import { render, screen, fireEvent } from '@testing-library/react'; import { Button } from './Button'; describe('Button', () => { // Define ALL expected behaviors upfront describe('rendering', () => { it('renders with label', () => { render( ); } ``` ### Extract Logic to Hooks ```typescript // useHome.ts - all logic here export function useHome() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const refresh = useCallback(async () => { setLoading(true); const data = await fetchItems(); setItems(data); setLoading(false); }, []); useEffect(() => { refresh(); }, [refresh]); return { items, loading, refresh }; } // HomePage.tsx - pure presentation export function HomePage(): JSX.Element { const { items, loading, refresh } = useHome(); if (loading) return ; return ; } ``` ### Props Interface Always Explicit ```typescript // Always define props interface, even if simple interface ItemCardProps { item: Item; onClick: (id: string) => void; } export function ItemCard({ item, onClick }: ItemCardProps): JSX.Element { return (
onClick(item.id)}>

{item.title}

); } ``` --- ## State Management ### Local State First ```typescript // Start with useState, escalate only when needed const [value, setValue] = useState(''); ``` ### Zustand for Global State (if needed) ```typescript // store/useAppStore.ts import { create } from 'zustand'; interface AppState { user: User | null; theme: 'light' | 'dark'; setUser: (user: User | null) => void; toggleTheme: () => void; } export const useAppStore = create((set) => ({ user: null, theme: 'light', setUser: (user) => set({ user }), toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })), })); ``` ### React Query for Server State ```typescript // api/queries/useItems.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { itemsApi } from '../client'; export function useItems() { return useQuery({ queryKey: ['items'], queryFn: itemsApi.getAll, staleTime: 5 * 60 * 1000, // 5 minutes }); } export function useCreateItem() { const queryClient = useQueryClient(); return useMutation({ mutationFn: itemsApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['items'] }); }, }); } ``` --- ## Routing ### React Router (Vite/CRA) ```typescript // App.tsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; export function App(): JSX.Element { return ( } /> } /> } /> ); } ``` ### Protected Routes ```typescript interface ProtectedRouteProps { children: JSX.Element; } function ProtectedRoute({ children }: ProtectedRouteProps): JSX.Element { const { user } = useAppStore(); const location = useLocation(); if (!user) { return ; } return children; } ``` --- ## Styling ### CSS Modules (Preferred) ```typescript // Button.module.css .primary { background: var(--color-primary); color: white; } .secondary { background: transparent; border: 1px solid var(--color-primary); } // Button.tsx import styles from './Button.module.css'; ``` ### Tailwind (Alternative) ```typescript // Use consistent patterns, extract repeated combinations const buttonVariants = { primary: 'bg-blue-500 text-white hover:bg-blue-600', secondary: 'bg-transparent border border-blue-500 text-blue-500', } as const; ``` --- ## Forms ### React Hook Form + Zod ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const schema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Password must be at least 8 characters'), }); type FormData = z.infer; export function LoginForm(): JSX.Element { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }); const onSubmit = (data: FormData) => { // handle submit }; return (
{errors.email && {errors.email.message}} {errors.password && {errors.password.message}}
); } ``` --- ## Testing ### Component Testing with React Testing Library ```typescript import { render, screen, fireEvent } from '@testing-library/react'; import { Button } from './Button'; describe('Button', () => { it('calls onClick when clicked', () => { const onClick = vi.fn(); render(