react-best-practices
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
childrenviaReact.PropsWithChildrenor explicitchildren: React.ReactNode. - Prefer specific prop types over
Record<string, unknown>orany. - 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
useReducerfor 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 someuseContextpatterns.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(), anduseCallback()— 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()+Suspensefor code-splitting large routes and heavy components. - Use
startTransitionfor expensive state updates that don't need immediate rendering. - Wrap
Suspenseboundaries 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/reactfor component tests. - Mock at the network boundary (e.g., MSW) rather than mocking hooks or internal state.
- Test custom hooks with
renderHookfrom@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.
More from grahamcrackers/skills
bulletproof-react-patterns
Bulletproof React architecture patterns for scalable, maintainable applications. Covers feature-based project structure, component patterns, state management boundaries, API layer design, error handling, security, and testing strategies. Use when structuring a React project, designing application architecture, organizing features, or when the user asks about React project structure or scalable patterns.
45react-aria-components
React Aria Components patterns for building accessible, unstyled UI with composition-based architecture. Covers component structure, styling with Tailwind and CSS, render props, collections, forms, selections, overlays, and drag-and-drop. Use when building accessible components, using react-aria-components, creating design systems, or when the user asks about React Aria, accessible UI primitives, or headless component libraries.
17clean-code-principles
Clean code principles for readable, maintainable TypeScript and React codebases. Covers naming, functions, abstraction, composition, error handling, comments, and code smells. Use when writing new code, refactoring, reviewing code quality, or when the user asks about clean code, readability, or maintainability.
10typescript-best-practices
Core TypeScript conventions for type safety, inference, and clean code. Use when writing TypeScript, reviewing TypeScript code, creating interfaces/types, or when the user asks about TypeScript patterns, conventions, or best practices.
9tanstack-query
TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.
8zustand
Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.
7