--- name: react-frontend description: Use when building React components — hooks, state management, TanStack Query, React Router, performance patterns, and testing. Triggers from .tsx/.jsx files and framework file patterns. --- # React Frontend Best Practices ## Component Patterns ### Composition over Inheritance ```typescript // ✅ Prefer composition function Layout({ sidebar, children }: { sidebar: ReactNode; children: ReactNode }) { return
{sidebar}
{children}
; } // ❌ Avoid inheritance patterns in React ``` ### Container/Presentational Separation ```typescript // Container: manages state, data fetching, business logic function UserProfileContainer() { const { data: user } = useUserQuery(userId); const { mutate: update } = useUpdateUserMutation(); return ; } // Presentational: pure rendering, props-only function UserProfile({ user, onUpdate }: UserProfileProps) { return
{user.name}
; } ``` ### Custom Hooks for Logic Extraction ```typescript // Extract reusable logic into custom hooks function useUserPermissions(userId: string) { const { data: user } = useUserQuery(userId); return useMemo(() => ({ isAdmin: user?.role === 'admin', canEdit: user?.role === 'admin' || user?.role === 'editor', canDelete: user?.role === 'admin', }), [user]); } ``` ## Hooks Rules - **Only call hooks at the top level** — not in conditions, loops, or callbacks. - **Only call hooks from React functions** — component or custom hook. - **Deps array matches reality** — include all values used inside. - **`useMemo`** for expensive computations. **`useCallback`** for stable references. - **`useEffect`** is for synchronization, not lifecycle. If you can compute from state, do it. ```typescript // ❌ Unnecessary effect const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); // ✅ Derived state const fullName = `${first} ${last}`; ``` ## Data Fetching (TanStack Query) ```typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Query function useUserQuery(id: string) { return useQuery({ queryKey: ['users', id], queryFn: () => api.users.get({ params: { id } }), staleTime: 30_000, // 30s before refetch gcTime: 5 * 60_000, // 5min cache }); } // Mutation with optimistic update function useUpdateUserMutation() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: UpdateUserInput) => api.users.update({ body: data }), onMutate: async (newUser) => { await queryClient.cancelQueries({ queryKey: ['users', newUser.id] }); const previous = queryClient.getQueryData(['users', newUser.id]); queryClient.setQueryData(['users', newUser.id], newUser); return { previous }; }, onError: (_, __, context) => { queryClient.setQueryData(['users', context.previous.id], context.previous); }, onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), }); } ``` **Rules:** - `staleTime` for read-through caching. Default 0 = refetch on mount. - `gcTime` for garbage collection of unused data. - `onMutate`/`onError`/`onSettled` for optimistic updates. - Queries over custom fetch + useEffect in all cases. ## State Management Selection | Need | Solution | |------|----------| | Server state | TanStack Query | | URL state | React Router / TanStack Router | | Form state | React Hook Form + Zod | | Client state (global) | Zustand or Context | | Client state (local) | `useState` / `useReducer` | | Component communication | Props / lifting state up | ```typescript // Zustand — lightweight, no boilerplate import { create } from 'zustand'; interface UIStore { sidebarOpen: boolean; toggleSidebar: () => void; } const useUIStore = create((set) => ({ sidebarOpen: true, toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })), })); ``` ## Routing (TanStack Router / React Router) ```typescript // TanStack Router — type-safe, modern const router = createRouter({ routeTree: rootRoute.addChildren([ indexRoute, usersRoute.addChildren([userRoute, userProfileRoute]), ]), }); ``` - **File-based routing** (Next.js App Router, Vite/Router) for simpler projects. - **Type-safe routers** (TanStack Router) for larger apps. - **Lazy load** route components — `React.lazy(() => import('./routes/Dashboard'))`. ## Performance - **Virtual lists** — `@tanstack/react-virtual` for 100+ items. - **React.memo** sparingly — only for components that re-render often with same props. - **`useMemo` for expensive calculations** — not for every value. - **Code splitting** — per route, per heavy component. - **Bundle analysis** — `vite-bundle-visualizer` to find bloat. ## Testing (Vitest + Testing Library) ```typescript import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserProfile } from './UserProfile'; describe('UserProfile', () => { it('renders user name', () => { render(); expect(screen.getByText('Alice')).toBeInTheDocument(); }); it('calls onUpdate when edit clicked', async () => { const onUpdate = vi.fn(); render(); await userEvent.click(screen.getByRole('button', { name: /edit/i })); expect(onUpdate).toHaveBeenCalledTimes(1); }); }); ``` - **Testing Library** for user-centric tests. Never test implementation details. - **userEvent** over `fireEvent` — simulates real user interactions. - **Component-level** tests for behavior, not storybook-style visual tests here. ## CSS / Styling - **Tailwind CSS** for utility-first styling. Consistent, fast, small. - **CSS Modules** when you need scoped component styles. - **CSS-in-JS** (styled-components, emotion) — only for dynamic theming. Prefer Tailwind. ## Anti-patterns - ❌ `useEffect` for data fetching — use TanStack Query - ❌ Prop drilling beyond 3 levels — compose or context - ❌ `useState` for derived data — compute from existing state - ❌ Direct DOM manipulation — use React refs - ❌ `any` in component props — always type props - ❌ Large component files — split by responsibility - ❌ `index` as key — breaks reconciliation on reorder - ❌ `useEffect` without deps — runs every render