---
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