|$ curl https://forge-ai.dev/api/markdown?path=docs/react/best-practices
$cat docs/react-—-best-practices-&-patterns.md
updated Recently·22 min read·published

React — Best Practices & Patterns

ReactAdvanced🎯Free Tools
Introduction

React is small on the surface — components, props, state, hooks — but production applications reveal a deep design space. The same library that lets you ship a todo list in an hour can power a team of fifty engineers building a streaming dashboard. Best practices are the bridge between those extremes.

This guide focuses on patterns that survive code review, scale across teams, and remain readable six months later. We cover component design, hooks discipline, state management, performance, security, testing, accessibility, and the move toward React Server Components. Each section includes practical examples you can apply today.

📝

note

These guidelines assume modern React: function components, hooks, and a build target of React 18/19. Class components still work but are no longer the recommended default for new code.
Dos and Don'ts

Before diving into patterns, a quick reference of the highest-leverage habits. Getting these right prevents the majority of performance bugs and maintenance issues in React codebases.

Don'tDo InsteadWhy
Index as keyUse stable, unique IDsPrevents reordering and state mismatch bugs
Memoize everythingProfile first, memoize hot pathsMemo has overhead; blind use hurts more than helps
useEffect for derived stateCompute during renderDeriving avoids stale state and extra renders
Prop drilling deep treesComposition or contextKeeps intermediate components decoupled
Mutating state directlyReplace state with new referencesImmutability is required for React change detection
Calling hooks conditionallyCall hooks at the top level alwaysPreserves hook call order across renders
Large componentsSplit by responsibilityEasier to test, review, and reuse
any everywhereType props and state explicitlyCatches integration bugs at compile time
dos-donts.tsx
TSX
1// Good — stable key, derived state, no mutation
2import { useMemo } from "react";
3
4type User = { id: string; name: string; role: "admin" | "user" };
5
6function UserList({ users }: { users: User[] }) {
7 // Derived during render — no useEffect needed
8 const admins = useMemo(
9 () => users.filter((u) => u.role === "admin"),
10 [users]
11 );
12
13 return (
14 <ul>
15 {admins.map((user) => (
16 <li key={user.id}>{user.name}</li> {/* stable key */}
17 ))}
18 </ul>
19 );
20}
21
22// Bad — index key, mutation, effect-derived state
23function UserListBad({ users }: { users: User[] }) {
24 const [admins, setAdmins] = React.useState<User[]>([]);
25
26 React.useEffect(() => {
27 users.forEach((u) => {
28 if (u.role === "admin") {
29 admins.push(u); // mutates state directly
30 }
31 });
32 setAdmins([...admins]);
33 }, [users]);
34
35 return (
36 <ul>
37 {admins.map((user, index) => (
38 <li key={index}>{user.name}</li> {/* fragile key */}
39 ))}
40 </ul>
41 );
42}
Code Style & Conventions

Consistent style removes friction in code review and lets developers focus on behavior. With TypeScript, conventions around naming, file structure, and types matter as much as formatting.

code-style.tsx
TSX
1// File: components/UserCard.tsx
2import type { ReactNode } from "react";
3
4// Props interface named after the component + "Props"
5interface UserCardProps {
6 id: string;
7 name: string;
8 avatar?: string;
9 children?: ReactNode;
10 onEdit: (id: string) => void;
11}
12
13// Function declaration for components (clearer hoisting behavior)
14export function UserCard({ id, name, avatar, children, onEdit }: UserCardProps) {
15 return (
16 <article className="user-card">
17 {avatar ? <img src={avatar} alt={`Avatar of ${name}`} /> : null}
18 <h3>{name}</h3>
19 {children}
20 <button type="button" onClick={() => onEdit(id)}>
21 Edit
22 </button>
23 </article>
24 );
25}
26
27// Avoid default exports for components; prefer named exports.
28// It makes refactoring and auto-imports more reliable.
29
30// Type event handlers narrowly instead of using any
31function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
32 event.preventDefault();
33}

best practice

Name files with PascalCase for components and camelCase for hooks and utilities. A component file should export one primary component; utilities can export multiple named functions. Avoid barrel files that re-export deeply nested modules — they hurt tree-shaking and add indirection.
custom-hook.tsx
TSX
1// Hooks are functions prefixed with "use" and may return a tuple or object
2import { useState, useCallback } from "react";
3
4interface UseCounterReturn {
5 count: number;
6 increment: () => void;
7 decrement: () => void;
8}
9
10export function useCounter(initial = 0): UseCounterReturn {
11 const [count, setCount] = useState(initial);
12
13 const increment = useCallback(() => setCount((c) => c + 1), []);
14 const decrement = useCallback(() => setCount((c) => c - 1), []);
15
16 return { count, increment, decrement };
17}
18
19// Use the hook
20export function Counter() {
21 const { count, increment } = useCounter(10);
22 return <button onClick={increment}>Count: {count}</button>;
23}
Project Structure

A scalable React project groups related files together. Colocation beats separation by file type because features are added and removed as units. Keep components shallow, co-locate tests and styles, and isolate side effects at the edges of the system.

project-structure.txt
TEXT
1my-app/
2 src/
3 app/ # Next.js App Router routes (or pages/)
4 layout.tsx
5 page.tsx
6 components/
7 ui/ # low-level reusable primitives
8 Button/
9 Button.tsx
10 Button.test.tsx
11 Button.module.css
12 index.ts
13 Card/
14 Card.tsx
15 Card.test.tsx
16 features/ # domain-specific components
17 UserProfile/
18 UserProfile.tsx
19 useUserProfile.ts
20 UserProfile.test.tsx
21 hooks/ # shared cross-cutting hooks
22 useLocalStorage.ts
23 useDebounce.ts
24 lib/ # pure utilities and config
25 api.ts
26 utils.ts
27 types/ # shared domain types
28 user.ts
29 styles/
30 globals.css
31 public/
32 package.json
33 tsconfig.json
34 eslint.config.mjs

The boundary between components/ui and components/features is important. UI components know nothing about your business domain. Feature components can depend on UI components, but UI components should never import feature components.

info

In Next.js, put page-specific code inside app/ route segments and keep heavy presentation logic in components/features. Route files should mostly compose components; avoid defining complex state directly in page.tsx unless it is truly route-scoped.
Hooks Discipline

Hooks are the core abstraction of modern React. The Rules of Hooks are not suggestions — React relies on their call order to associate state with components. Break them and you get subtle, hard-to-debug failures.

hooks-discipline.tsx
TSX
1// Good — hooks called at top level, stable dependencies
2import { useEffect, useState } from "react";
3
4function Search({ query }: { query: string }) {
5 const [results, setResults] = useState<string[]>([]);
6
7 useEffect(() => {
8 const controller = new AbortController();
9
10 fetch(`/api/search?q=${encodeURIComponent(query)}`, {
11 signal: controller.signal,
12 })
13 .then((res) => res.json())
14 .then((data) => setResults(data.results))
15 .catch((err) => {
16 if (err.name !== "AbortError") console.error(err);
17 });
18
19 return () => controller.abort();
20 }, [query]);
21
22 return (
23 <ul>
24 {results.map((item) => (
25 <li key={item}>{item}</li>
26 ))}
27 </ul>
28 );
29}
30
31// Bad — hook inside condition, missing cleanup
32function SearchBad({ query }: { query: string }) {
33 if (query) {
34 // ❌ never call hooks conditionally
35 const [results, setResults] = useState([]);
36 }
37
38 useEffect(() => {
39 fetch(`/api/search?q=${query}`)
40 .then((res) => res.json())
41 .then(setResults);
42 // ❌ no cleanup and no dependency array
43 });
44}

warning

Treat the dependency array of useEffect as a contract, not a nuisance. If ESLint asks you to add a dependency, add it. If that causes infinite loops, your dependency is likely unstable or your effect is doing too much. Split the effect rather than lying to React.
State Management

Start with local state. Lift state only when a sibling needs it. Reach for context when many components at different nesting levels need the same data. Only add a global store when you have genuinely global, frequently accessed state or complex cross-cutting update logic.

state-management.tsx
TSX
1// Local state is enough here
2function LikeButton() {
3 const [liked, setLiked] = useState(false);
4 return (
5 <button onClick={() => setLiked((prev) => !prev)}>
6 {liked ? "♥" : "♡"}
7 </button>
8 );
9}
10
11// Lifted state for shared sibling data
12function CounterParent() {
13 const [count, setCount] = useState(0);
14 return (
15 <div>
16 <CounterDisplay count={count} />
17 <CounterControls onChange={setCount} />
18 </div>
19 );
20}
21
22// Context for theme/settings that many components consume
23import { createContext, useContext, useState } from "react";
24
25interface ThemeContextValue {
26 theme: "light" | "dark";
27 toggle: () => void;
28}
29
30const ThemeContext = createContext<ThemeContextValue | null>(null);
31
32export function ThemeProvider({ children }: { children: React.ReactNode }) {
33 const [theme, setTheme] = useState<"light" | "dark">("dark");
34 const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark"));
35 return (
36 <ThemeContext.Provider value={{ theme, toggle }}>
37 {children}
38 </ThemeContext.Provider>
39 );
40}
41
42export function useTheme() {
43 const ctx = useContext(ThemeContext);
44 if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
45 return ctx;
46}

Keep state as low in the tree as possible. State placed too high causes unnecessary re-renders and couples unrelated components. Similarly, avoid duplicating state that can be derived from props or existing state.

Performance

React is fast by default. Most performance problems come from unnecessary renders, large lists, or expensive computations running on every render. Measure before optimizing. The React DevTools Profiler is your first stop.

performance.tsx
TSX
1import { memo, useMemo, useCallback } from "react";
2
3interface ExpensiveListProps {
4 items: { id: string; name: string; value: number }[];
5 onSelect: (id: string) => void;
6}
7
8// Memoize a child whose parent re-renders often
9const ListItem = memo(function ListItem({
10 item,
11 onSelect,
12}: {
13 item: ExpensiveListProps["items"][number];
14 onSelect: (id: string) => void;
15}) {
16 return (
17 <li>
18 <button onClick={() => onSelect(item.id)}>
19 {item.name}: {item.value}
20 </button>
21 </li>
22 );
23});
24
25export function ExpensiveList({ items, onSelect }: ExpensiveListProps) {
26 // Memoize expensive derived data
27 const total = useMemo(
28 () => items.reduce((sum, item) => sum + item.value, 0),
29 [items]
30 );
31
32 // Stable callback so memoized children don't re-render unnecessarily
33 const handleSelect = useCallback(
34 (id: string) => onSelect(id),
35 [onSelect]
36 );
37
38 return (
39 <div>
40 <p>Total: {total}</p>
41 <ul>
42 {items.map((item) => (
43 <ListItem key={item.id} item={item} onSelect={handleSelect} />
44 ))}
45 </ul>
46 </div>
47 );
48}

info

memo only helps when props are stable. If the parent passes new object or function references on every render, memo bails out before diffing but re-renders anyway. Combine memo with useMemo and useCallback for the children that matter.
virtualization-lazy.tsx
TSX
1// Virtualize long lists instead of rendering all rows
2import { FixedSizeList as List } from "react-window";
3
4function VirtualizedList({ items }: { items: string[] }) {
5 const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
6 <div style={style}>{items[index]}</div>
7 );
8
9 return (
10 <List
11 height={400}
12 itemCount={items.length}
13 itemSize={35}
14 width="100%"
15 >
16 {Row}
17 </List>
18 );
19}
20
21// Lazy load routes and heavy components to reduce initial bundle size
22import { lazy, Suspense } from "react";
23
24const Dashboard = lazy(() => import("./Dashboard"));
25
26function App() {
27 return (
28 <Suspense fallback={<Spinner />}>
29 <Dashboard />
30 </Suspense>
31 );
32}
Composition Patterns

Composition is React's superpower. It lets you build flexible APIs without prop explosion, and it keeps components decoupled. Prefer children and slots over configuration objects when the consumer needs control over rendered output.

composition.tsx
TSX
1// Flexible layout with composition instead of many props
2import type { ReactNode } from "react";
3
4interface CardProps {
5 title: string;
6 children: ReactNode;
7 actions?: ReactNode;
8}
9
10export function Card({ title, children, actions }: CardProps) {
11 return (
12 <div className="card">
13 <h2>{title}</h2>
14 <div className="card-body">{children}</div>
15 {actions ? <div className="card-actions">{actions}</div> : null}
16 </div>
17 );
18}
19
20// Usage
21<Card
22 title="Project Settings"
23 actions={
24 <>
25 <button>Cancel</button>
26 <button>Save</button>
27 </>
28 }
29>
30 <p>Configure your project here.</p>
31</Card>
32
33// Compound components for tightly related pieces
34import { createContext, useContext, useState } from "react";
35
36const TabsContext = createContext<{
37 active: string;
38 setActive: (value: string) => void;
39} | null>(null);
40
41export function Tabs({
42 defaultTab,
43 children,
44}: {
45 defaultTab: string;
46 children: ReactNode;
47}) {
48 const [active, setActive] = useState(defaultTab);
49 return (
50 <TabsContext.Provider value={{ active, setActive }}>
51 {children}
52 </TabsContext.Provider>
53 );
54}
55
56Tabs.List = function List({ children }: { children: ReactNode }) {
57 return <div role="tablist">{children}</div>;
58};
59
60Tabs.Tab = function Tab({ value, children }: { value: string; children: ReactNode }) {
61 const ctx = useContext(TabsContext);
62 if (!ctx) throw new Error("Tab must be inside Tabs");
63 return (
64 <button
65 role="tab"
66 aria-selected={ctx.active === value}
67 onClick={() => ctx.setActive(value)}
68 >
69 {children}
70 </button>
71 );
72};
73
74Tabs.Panel = function Panel({ value, children }: { value: string; children: ReactNode }) {
75 const ctx = useContext(TabsContext);
76 if (!ctx) throw new Error("Panel must be inside Tabs");
77 return ctx.active === value ? <div role="tabpanel">{children}</div> : null;
78};
React Server Components

React Server Components (RSC) move data fetching and heavy rendering to the server. They are not a replacement for client components; they are a new primitive for building applications. In frameworks like Next.js, components are server components by default in the App Router.

server-components.tsx
TSX
1// Server Component — fetches data directly, zero client JS
2async function PostList() {
3 const posts = await fetch("https://api.example.com/posts", {
4 cache: "force-cache",
5 }).then((res) => res.json());
6
7 return (
8 <ul>
9 {posts.map((post: { id: string; title: string }) => (
10 <li key={post.id}>{post.title}</li>
11 ))}
12 </ul>
13 );
14}
15
16// Client Component for interactivity
17"use client";
18
19import { useState } from "react";
20
21function LikeButton({ postId }: { postId: string }) {
22 const [liked, setLiked] = useState(false);
23
24 return (
25 <button onClick={() => setLiked((prev) => !prev)}>
26 {liked ? "Unlike" : "Like"} {postId}
27 </button>
28 );
29}
30
31// Compose server and client components; pass server data as props
32async function PostPage({ params }: { params: { id: string } }) {
33 const post = await fetch(`https://api.example.com/posts/${params.id}`).then(
34 (res) => res.json()
35 );
36
37 return (
38 <article>
39 <h1>{post.title}</h1>
40 <p>{post.body}</p>
41 <LikeButton postId={post.id} />
42 </article>
43 );
44}

best practice

Keep client components small and leaf-node. Fetch data, access databases, and perform authorization in server components. Only add "use client" when you need browser APIs, event handlers, or React hooks. The boundary between server and client is a bundle boundary — crossing it has cost.
Security

React provides some XSS protection by escaping interpolated values, but applications still face injection risks through dangerouslySetInnerHTML, unsafe URLs, unvalidated form submissions, and leaked secrets in client bundles.

security.tsx
TSX
1// Dangerous — only use when content is fully trusted and sanitized
2function SafeHtml({ html }: { html: string }) {
3 // html must be sanitized by DOMPurify or similar on the server
4 return <div dangerouslySetInnerHTML={{ __html: html }} />;
5}
6
7// Safe — React escapes text automatically
8function UserComment({ text }: { text: string }) {
9 return <p>{text}</p>; // <script> tags are rendered as text, not executed
10}
11
12// Validate URLs before rendering them
13function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) {
14 const isSafe = href.startsWith("https://") || href.startsWith("/");
15 if (!isSafe) return null;
16 return (
17 <a href={href} target="_blank" rel="noopener noreferrer">
18 {children}
19 </a>
20 );
21}
22
23// Never put secrets in client bundles
24// ❌ Bad: Vite exposes env vars prefixed with VITE_ to the client
25const API_KEY = import.meta.env.VITE_SECRET_API_KEY; // visible in bundle
26
27// ✅ Good: keep secrets in server components, route handlers, or env files
28// loaded only by the server runtime
29async function ServerData() {
30 const data = await fetch("https://api.example.com/data", {
31 headers: { Authorization: `Bearer ${process.env.API_KEY}` },
32 });
33 return <pre>{JSON.stringify(await data.json(), null, 2)}</pre>;
34}

warning

Avoid dangerouslySetInnerHTML whenever possible. If you must use it, sanitize the HTML server-side with a library like DOMPurify, and never pass user input into it directly. Treat it with the same caution as eval.
Testing

Test behavior, not implementation. React Testing Library encourages tests that resemble how users interact with your application. Avoid testing internal state or asserting on exact component structure; those tests break during refactoring.

testing.tsx
TSX
1import { render, screen, fireEvent } from "@testing-library/react";
2import { Counter } from "./Counter";
3
4test("increments count when button is clicked", () => {
5 render(<Counter />);
6
7 const button = screen.getByRole("button", { name: /count/i });
8 expect(button).toHaveTextContent("Count: 0");
9
10 fireEvent.click(button);
11 expect(button).toHaveTextContent("Count: 1");
12});
13
14// Custom hook test with renderHook
15import { renderHook, act } from "@testing-library/react";
16import { useCounter } from "./useCounter";
17
18test("useCounter increments and decrements", () => {
19 const { result } = renderHook(() => useCounter(5));
20
21 act(() => result.current.increment());
22 expect(result.current.count).toBe(6);
23
24 act(() => result.current.decrement());
25 expect(result.current.count).toBe(5);
26});
27
28// MSW for network mocking
29import { http, HttpResponse } from "msw";
30import { server } from "./mocks/server";
31
32test("displays user after loading", async () => {
33 server.use(
34 http.get("/api/user", () => {
35 return HttpResponse.json({ name: "Ada" });
36 })
37 );
38
39 render(<UserProfile userId="1" />);
40 expect(await screen.findByText("Ada")).toBeInTheDocument();
41});

info

Write tests from the user's perspective: query by role, label, or text. Use findBy for async elements, getBy for synchronous ones, and queryBy when asserting absence. Mock service workers (MSW) keep your tests resilient against API changes.
Accessibility

Accessible React apps are better for everyone and required by law in many jurisdictions. Most accessibility issues are caused by incorrect markup, missing labels, or keyboard traps rather than React itself. Use semantic HTML and manage focus deliberately.

accessibility.tsx
TSX
1// Accessible form with labels and error messages
2function LoginForm() {
3 return (
4 <form aria-label="Login form">
5 <div>
6 <label htmlFor="email">Email</label>
7 <input
8 id="email"
9 type="email"
10 name="email"
11 aria-required="true"
12 aria-describedby="email-error"
13 />
14 <span id="email-error" role="alert">
15 Invalid email
16 </span>
17 </div>
18 <button type="submit">Sign in</button>
19 </form>
20 );
21}
22
23// Use useId for stable unique IDs in reusable components
24import { useId } from "react";
25
26function TextField({ label }: { label: string }) {
27 const id = useId();
28 return (
29 <div>
30 <label htmlFor={id}>{label}</label>
31 <input id={id} type="text" />
32 </div>
33 );
34}
35
36// Manage focus after actions, especially modal open/close
37import { useRef, useEffect } from "react";
38
39function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
40 const ref = useRef<HTMLDialogElement>(null);
41
42 useEffect(() => {
43 if (isOpen) ref.current?.showModal();
44 else ref.current?.close();
45 }, [isOpen]);
46
47 return (
48 <dialog ref={ref} onClose={onClose}>
49 <button onClick={onClose}>Close</button>
50 </dialog>
51 );
52}

best practice

Run automated accessibility checks with tools like Axe or @axe-core/react, but do not rely on them alone. Test keyboard navigation manually and, when possible, with a screen reader. The dialog element and native form controls solve many problems that custom components recreate poorly.
Tooling

A modern React project benefits from a toolchain that catches errors early and enforces consistency. TypeScript, ESLint, and Prettier are the baseline. Add testing, bundle analysis, and CI checks as the project matures.

tsconfig.json
JSON
1// tsconfig.json — strict mode recommended for React projects
2{
3 "compilerOptions": {
4 "target": "ES2022",
5 "lib": ["dom", "dom.iterable", "esnext"],
6 "allowJs": false,
7 "skipLibCheck": true,
8 "strict": true,
9 "noUncheckedIndexedAccess": true,
10 "exactOptionalPropertyTypes": true,
11 "forceConsistentCasingInFileNames": true,
12 "module": "ESNext",
13 "moduleResolution": "bundler",
14 "resolveJsonModule": true,
15 "isolatedModules": true,
16 "jsx": "preserve",
17 "incremental": true,
18 "paths": {
19 "@/*": ["./src/*"]
20 }
21 },
22 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
23 "exclude": ["node_modules"]
24}
package.json
JSON
1// package.json — recommended scripts and dependencies
2{
3 "scripts": {
4 "dev": "next dev",
5 "build": "next build",
6 "start": "next start",
7 "lint": "next lint",
8 "typecheck": "tsc --noEmit",
9 "test": "vitest",
10 "test:e2e": "playwright test",
11 "analyze": "cross-env ANALYZE=true next build"
12 },
13 "devDependencies": {
14 "@testing-library/react": "^16.x",
15 "@types/react": "^19.x",
16 "eslint": "^9.x",
17 "eslint-config-next": "latest",
18 "prettier": "^3.x",
19 "typescript": "^5.x",
20 "vitest": "^3.x"
21 }
22}

info

Use the React DevTools Profiler to find actual rendering bottlenecks, and @next/bundle-analyzer to inspect bundle size. Optimize based on measurements, not assumptions.
Code Review Checklist

A structured review checklist keeps quality consistent across a team. Automate the easy parts with CI; reserve human review for architecture, correctness, and user experience.

CheckAutomated?Details
Type checktsc --noEmitNo implicit any, props typed
LinteslintRules of hooks, exhaustive deps
FormatprettierConsistent code style
Testsvitest / jestNew behavior covered
Keyseslint / reviewStable IDs, not array index
Hook depseslint-plugin-react-hooksDependency arrays correct
Accessibilityaxe / reviewLabels, roles, keyboard flow
SecurityreviewNo secrets, sanitized HTML
Performanceprofiler / reviewMemoization justified
Component sizereviewSingle responsibility
ci.yml
YAML
1# .github/workflows/ci.yml example
2name: CI
3on: [push, pull_request]
4jobs:
5 check:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with:
11 node-version: 22
12 cache: npm
13 - run: npm ci
14 - run: npm run lint
15 - run: npm run typecheck
16 - run: npm test -- --run
17 - run: npm run build
Resources

The React ecosystem evolves quickly. Refer to the official sources for authoritative guidance and keep your dependencies current without chasing every release.

ResourceWhat it covers
react.devOfficial docs, hooks, thinking in React, RSC
React TypeScript CheatsheetCommon React + TS patterns and gotchas
Testing Library docsGuiding principles and query priorities
WCAG 2.2Web Content Accessibility Guidelines
Next.js docsApp Router, caching, server components
$Blueprint — Engineering Documentation·Section ID: REACT-BP·Revision: 1.0