---
name: frontend
description: Use when making architecture-level React decisions — component decomposition, choosing where state should live, selecting a state manager (Context vs Redux vs Zustand) or a data-fetching library (TanStack Query vs SWR), or planning a rendering-performance budget. For advanced hook patterns, use react.
---
# Frontend Patterns
Conventions and best practices for building maintainable, performant React applications.
## When to Activate
- Designing component structure or deciding where state should live
- Choosing between local state, Context, Zustand, or Redux Toolkit
- Implementing data fetching, caching, or server state synchronization
- Building forms with validation and submission handling
- Optimizing rendering performance (re-renders, bundle size, lazy loading)
- Setting up routing with protected routes or nested layouts
- Writing tests for React components and hooks
## Component Design
### Single Responsibility
```tsx
// BAD: component does too many things
function UserDashboard({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [orders, setOrders] = useState([]);
useEffect(() => { /* fetch user + orders */ }, []);
return
);
}
```
## Testing
### Component Tests (React Testing Library)
```tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("submits form with valid data", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render();
await user.type(screen.getByLabelText("Email"), "alice@example.com");
await user.type(screen.getByLabelText("Password"), "secretpass");
await user.click(screen.getByRole("button", { name: "Create User" }));
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "secretpass",
}));
});
test("shows validation errors", async () => {
const user = userEvent.setup();
render();
await user.click(screen.getByRole("button", { name: "Create User" }));
expect(await screen.findByText("Invalid email")).toBeInTheDocument();
});
```
### Testing Hooks
```tsx
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
```
> See also: `unit-testing`, `accessibility`
## Red Flags
- **Data fetching inside `useEffect` without a library** — manual fetch-in-effect produces race conditions, missing loading/error states, and no deduplication; use TanStack Query or SWR
- **Global state for server data** — storing server-fetched data in Redux/Zustand duplicates cache logic already solved by a data fetching library; keep server state in the fetching layer
- **`key={index}` in lists** — using array index as key breaks React reconciliation when items reorder or are inserted; use a stable, unique ID from the data
- **Uncontrolled forms for complex validation** — uncontrolled inputs with `ref` can't drive real-time validation or conditional fields; use React Hook Form with Zod schema validation
- **`useEffect` to sync derived state** — computing derived values in an effect causes an extra render cycle; compute them inline during render or memoize with `useMemo`
- **Prop drilling more than 2 levels** — passing props through 3+ components is a sign the tree needs restructuring or a context/selector; don't reach for global state before considering composition
- **No `Suspense` boundary around lazy-loaded routes** — code-split routes without a fallback show a blank screen during load; wrap every lazy route in `}>`
## Checklist
- [ ] Components have a single clear responsibility — split if rendering + fetching + formatting
- [ ] Server state managed with TanStack Query, not `useEffect` + `useState`
- [ ] Forms use React Hook Form with Zod schema validation
- [ ] Shared client state uses Zustand (simple) or Redux Toolkit (complex)
- [ ] URL state (filters, pagination, selected ID) stored in search params
- [ ] Route-level code splitting applied to all page components
- [ ] Lists over ~100 items virtualized
- [ ] `useMemo` / `useCallback` / `memo` applied only where profiling confirms re-render cost
- [ ] Components tested via React Testing Library (user interactions, not implementation)
- [ ] Accessible: semantic HTML, labels on inputs, keyboard navigation verified
- [ ] No prop drilling beyond 2 levels — use composition, Context, or Zustand