React Best Practices
Component Design
- One component per file. Name the file the same as the component.
- Prefer function components — never write class components.
- Keep components small and focused. If a component exceeds ~150 lines, split it.
- Define components at module scope — never define components inside other components (breaks state preservation and identity).
- Colocate related files (component, hook, types, styles, tests) in the same directory.
Props
- Destructure props in the function signature:
function UserCard({ name, email, avatar }: UserCardProps) { ... }
- Use
children via React.PropsWithChildren or explicit children: React.ReactNode.
- Prefer specific prop types over
Record<string, unknown> or any.
- Avoid prop drilling beyond 2-3 levels — use composition, context, or a state management solution.
Hooks
- Follow the Rules of Hooks: only call at the top level, only in React components or custom hooks.
- Extract reusable logic into custom hooks (
use* prefix).
- Keep hooks focused — a hook that does too many things should be split.
State Management
- Keep state as local as possible. Lift only when sibling components need it.
- Use
useReducer for complex state with multiple related transitions.
- Reserve context for truly global concerns (theme, auth, locale) — not frequently changing data.
- For server state, use TanStack Query or a similar library instead of
useState + useEffect.
React 19 Hooks
use(): Read context or unwrap promises inside components. Replaces some useContext patterns.
useActionState(): Manage form submission state (pending, error, result) without manual boilerplate.
useOptimistic(): Show optimistic UI immediately during async operations.
useTransition(): Wrap non-urgent state updates to keep the UI responsive.
React 19 Patterns
Automatic Memoization (React Compiler)
When using the React Compiler:
- Remove manual
React.memo(), useMemo(), and useCallback() — the compiler handles memoization automatically.
- If not using the compiler, still be intentional — only memoize when profiling reveals a real performance issue.
Actions and Forms
Use React 19 Actions for form handling:
function CreatePost() {
const [state, action, isPending] = useActionState(createPostAction, null);
return (
<form action={action}>
<input name="title" required />
<button type="submit" disabled={isPending}>
Create
</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
Server Components
- Default to server components for non-interactive content (data display, lists, static layouts).
- Add
"use client" only when the component needs interactivity (event handlers, hooks, browser APIs).
- Push
"use client" boundaries as far down the tree as possible.
Performance
- Use
React.lazy() + Suspense for code-splitting large routes and heavy components.
- Use
startTransition for expensive state updates that don't need immediate rendering.
- Wrap
Suspense boundaries around async data fetching to avoid waterfall loading.
- Avoid creating new objects/arrays in JSX props on every render — extract to constants or memoize.
Patterns
Composition over Configuration
Prefer composable children over giant config props:
// Prefer
<Dialog>
<Dialog.Header>Title</Dialog.Header>
<Dialog.Body>Content</Dialog.Body>
<Dialog.Footer>
<Button>Close</Button>
</Dialog.Footer>
</Dialog>
// Avoid
<Dialog
header="Title"
body="Content"
footer={<Button>Close</Button>}
/>
Render Props & Children as Function
Use when a component needs to share render-time data without prescribing UI:
<DataLoader query={userQuery}>{(data) => <UserProfile user={data} />}</DataLoader>
Custom Hook + Component Pairs
Separate logic from presentation:
function useUserSearch(query: string) {
// filtering, debouncing, API calls
return { results, isLoading, error };
}
function UserSearch() {
const [query, setQuery] = useState("");
const { results, isLoading } = useUserSearch(query);
// render
}
Error Handling
- Use Error Boundaries to catch render errors and show fallback UI.
- Don't use Error Boundaries for event handler errors — use try/catch in the handler.
- Provide meaningful fallback UI, not blank screens.
Testing
- Test behavior, not implementation. Query by role, label, or text — not by test IDs or CSS classes.
- Use
@testing-library/react for component tests.
- Mock at the network boundary (e.g., MSW) rather than mocking hooks or internal state.
- Test custom hooks with
renderHook from @testing-library/react.
Project Structure
src/
├── components/ # shared/reusable components
│ └── button/
│ ├── button.tsx
│ ├── button.test.tsx
│ └── index.ts
├── features/ # feature-specific code (components, hooks, utils)
│ └── auth/
├── hooks/ # shared custom hooks
├── lib/ # utilities, API clients, helpers
├── types/ # shared TypeScript types
└── app/ # routes / pages
Colocate feature-specific code in features/ and only promote to components/ or hooks/ when reused across features.
1---2name: react-best-practices3description: Modern React 19 patterns for components, hooks, state management, performance, and project structure. Use when writing React components, reviewing React code, designing component APIs, or when the user asks about React conventions, architecture, or best practices.4---56# React Best Practices78## Component Design910- One component per file. Name the file the same as the component.11- Prefer function components — never write class components.12- Keep components small and focused. If a component exceeds ~150 lines, split it.13- Define components at module scope — never define components inside other components (breaks state preservation and identity).14- Colocate related files (component, hook, types, styles, tests) in the same directory.1516## Props1718- Destructure props in the function signature:1920```tsx21function UserCard({ name, email, avatar }: UserCardProps) { ... }22```2324- Use `children` via `React.PropsWithChildren` or explicit `children: React.ReactNode`.25- Prefer specific prop types over `Record<string, unknown>` or `any`.26- Avoid prop drilling beyond 2-3 levels — use composition, context, or a state management solution.2728## Hooks2930- Follow the Rules of Hooks: only call at the top level, only in React components or custom hooks.31- Extract reusable logic into custom hooks (`use*` prefix).32- Keep hooks focused — a hook that does too many things should be split.3334### State Management3536- Keep state as local as possible. Lift only when sibling components need it.37- Use `useReducer` for complex state with multiple related transitions.38- Reserve context for truly global concerns (theme, auth, locale) — not frequently changing data.39- For server state, use TanStack Query or a similar library instead of `useState` + `useEffect`.4041### React 19 Hooks4243- **`use()`**: Read context or unwrap promises inside components. Replaces some `useContext` patterns.44- **`useActionState()`**: Manage form submission state (pending, error, result) without manual boilerplate.45- **`useOptimistic()`**: Show optimistic UI immediately during async operations.46- **`useTransition()`**: Wrap non-urgent state updates to keep the UI responsive.4748## React 19 Patterns4950### Automatic Memoization (React Compiler)5152When using the React Compiler:5354- Remove manual `React.memo()`, `useMemo()`, and `useCallback()` — the compiler handles memoization automatically.55- If not using the compiler, still be intentional — only memoize when profiling reveals a real performance issue.5657### Actions and Forms5859Use React 19 Actions for form handling:6061```tsx62function CreatePost() {63 const [state, action, isPending] = useActionState(createPostAction, null);6465 return (66 <form action={action}>67 <input name="title" required />68 <button type="submit" disabled={isPending}>69 Create70 </button>71 {state?.error && <p>{state.error}</p>}72 </form>73 );74}75```7677### Server Components7879- Default to server components for non-interactive content (data display, lists, static layouts).80- Add `"use client"` only when the component needs interactivity (event handlers, hooks, browser APIs).81- Push `"use client"` boundaries as far down the tree as possible.8283## Performance8485- Use `React.lazy()` + `Suspense` for code-splitting large routes and heavy components.86- Use `startTransition` for expensive state updates that don't need immediate rendering.87- Wrap `Suspense` boundaries around async data fetching to avoid waterfall loading.88- Avoid creating new objects/arrays in JSX props on every render — extract to constants or memoize.8990## Patterns9192### Composition over Configuration9394Prefer composable children over giant config props:9596```tsx97// Prefer98<Dialog>99 <Dialog.Header>Title</Dialog.Header>100 <Dialog.Body>Content</Dialog.Body>101 <Dialog.Footer>102 <Button>Close</Button>103 </Dialog.Footer>104</Dialog>105106// Avoid107<Dialog108 header="Title"109 body="Content"110 footer={<Button>Close</Button>}111/>112```113114### Render Props & Children as Function115116Use when a component needs to share render-time data without prescribing UI:117118```tsx119<DataLoader query={userQuery}>{(data) => <UserProfile user={data} />}</DataLoader>120```121122### Custom Hook + Component Pairs123124Separate logic from presentation:125126```tsx127function useUserSearch(query: string) {128 // filtering, debouncing, API calls129 return { results, isLoading, error };130}131132function UserSearch() {133 const [query, setQuery] = useState("");134 const { results, isLoading } = useUserSearch(query);135 // render136}137```138139## Error Handling140141- Use Error Boundaries to catch render errors and show fallback UI.142- Don't use Error Boundaries for event handler errors — use try/catch in the handler.143- Provide meaningful fallback UI, not blank screens.144145## Testing146147- Test behavior, not implementation. Query by role, label, or text — not by test IDs or CSS classes.148- Use `@testing-library/react` for component tests.149- Mock at the network boundary (e.g., MSW) rather than mocking hooks or internal state.150- Test custom hooks with `renderHook` from `@testing-library/react`.151152## Project Structure153154```155src/156├── components/ # shared/reusable components157│ └── button/158│ ├── button.tsx159│ ├── button.test.tsx160│ └── index.ts161├── features/ # feature-specific code (components, hooks, utils)162│ └── auth/163├── hooks/ # shared custom hooks164├── lib/ # utilities, API clients, helpers165├── types/ # shared TypeScript types166└── app/ # routes / pages167```168169Colocate feature-specific code in `features/` and only promote to `components/` or `hooks/` when reused across features.