| name | react-patterns |
| description | React best practices covering functional components, hooks, state management, performance, and accessibility. Use when writing or reviewing React components, hooks, or state logic. |
React Patterns
Best practices for React development: functional components, hooks, state management, performance, and accessibility.
Component Rules
- Always use functional components -- never class components for new code
- One component per file -- name the file the same as the component (PascalCase)
- Export components as named exports -- default exports only for pages/routes
- Colocate related files -- keep
Component.tsx, Component.test.tsx, and Component.module.css together
- Props interface above the component -- name it
ComponentNameProps
interface UserCardProps {
user: User;
onSelect: (id: string) => void;
variant?: "compact" | "full";
}
export function UserCard({ user, onSelect, variant = "full" }: UserCardProps) {
return ( );
}
export default class UserCard extends React.Component<{user: any}> { }
Hooks Patterns
Custom Hooks
- Prefix with
use -- useAuth, useDebounce, useLocalStorage
- Extract shared logic into custom hooks -- if two components share stateful logic, extract it
- Return tuples for simple hooks, objects for complex ones
function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle];
}
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
return { data, error, loading, refetch };
}
Hook Rules
- Never call hooks conditionally -- all hooks must run on every render
- Use
useCallback for functions passed to children -- prevents unnecessary re-renders
- Use
useMemo only for expensive computations -- don't wrap everything
- Prefer
useReducer over useState when state transitions are complex
useEffect Guidelines
- Always specify dependencies -- never use
// eslint-disable-next-line
- Return a cleanup function for subscriptions, timers, and listeners
- Avoid setting state in useEffect when you can derive it -- computed values don't need effects
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
const fullName = `${firstName} ${lastName}`;
State Management Tiers
Use the simplest tier that solves the problem:
| Tier | Tool | When to Use |
|---|
| 1. Local state | useState, useReducer | Single component state |
| 2. Lifted state | Props, composition | Shared between parent/child |
| 3. Context | createContext + useContext | Theme, auth, locale -- rarely changes |
| 4. URL state | Search params, path params | Filters, pagination, navigation state |
| 5. Server state | React Query / SWR | API data, caching, synchronization |
| 6. Global store | Zustand / Redux Toolkit | Complex client state across many components |
Context Guidelines
- Split contexts by domain --
AuthContext, ThemeContext, not AppContext
- Keep context values stable -- use
useMemo on the provider value
- Don't put frequently changing values in context -- it re-renders all consumers
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Performance
- Use React.lazy + Suspense for route-level code splitting
- Memoize expensive list items with
React.memo -- include a custom comparator if props are objects
- Virtualize long lists -- use
react-window or @tanstack/virtual for 100+ items
- Avoid anonymous functions in JSX when passing to memoized children
- Use
key correctly -- stable, unique identifiers, never array index for dynamic lists
const Dashboard = lazy(() => import("./pages/Dashboard"));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
Accessibility
- Use semantic HTML --
<button> not <div onClick>, <nav> not <div className="nav">
- All images need alt text -- empty
alt="" for decorative images
- Form inputs need labels -- use
<label htmlFor> or aria-label
- Manage focus on route changes -- focus the main heading after navigation
- Support keyboard navigation -- all interactive elements must be reachable via Tab
- Use ARIA attributes when semantic HTML is insufficient --
aria-expanded, aria-live, role
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">{title}</h2>
{children}
<button ref={closeRef} onClick={onClose}>
Close
</button>
</div>
);
}
Error Boundaries
- Wrap route-level components in error boundaries
- Provide a meaningful fallback UI -- not just "Something went wrong"
- Log errors to your monitoring service in the boundary
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Dashboard />
</ErrorBoundary>
Anti-patterns
- Prop drilling more than 2 levels -- use composition or context instead
- Giant useEffect blocks -- split into multiple focused effects
- Storing derived state -- compute it during render
- Premature optimization -- profile before adding
useMemo/useCallback everywhere
- String-based refs -- use
useRef only
- Direct DOM manipulation -- use refs only when React APIs are insufficient