React Patterns & Best Practices
Core Principles
- Composition over inheritance — Build complex UIs by combining small, focused components
- Hooks for all logic — Use hooks to encapsulate stateful logic and side effects. Never use class components
- Unidirectional data flow — Data flows down via props; events flow up via callbacks
- Colocation — Place state, styles, tests, and types next to the components that use them
- Type safety everywhere — Use TypeScript for props, state, context, and hook return types
Hooks Fundamentals
function Search({ items, onSelect }: { items: string[]; onSelect: (item: string) => void }) {
const [query, setQuery] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const filtered = useMemo(
() => items.filter((item) => item.toLowerCase().includes(query.toLowerCase())),
[items, query]
);
const handleSelect = useCallback(
(item: string) => { setQuery(""); onSelect(item); },
[onSelect]
);
useEffect(() => { inputRef.current?.focus(); }, []);
return (
<div>
<input ref={inputRef} value={query} => setQuery(e.target.value)} />
{filtered.map((item) => (
<button key={item} => handleSelect(item)}>{item}</button>
))}
</div>
);
}
See hooks-patterns for:
- Rules of hooks, useId, useRef patterns
- Cleanup functions and dependency array pitfalls
Custom Hooks
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); })
.then((json) => setData(json as T))
.catch((err) => { if (err.name !== "AbortError") setError(err); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}
See custom-hooks for:
- Form hooks, event listener hooks, debounce hooks, and hook composition
State Management
type CartAction =
| { type: "ADD_ITEM"; payload: { id: string; name: string; price: number } }
| { type: "REMOVE_ITEM"; payload: { id: string } }
| { type: "CLEAR" };
interface CartState { items: Array<{ id: string; name: string; price: number }> }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case "ADD_ITEM": return { items: [...state.items, action.payload] };
case "REMOVE_ITEM": return { items: state.items.filter((i) => i.id !== action.payload.id) };
case "CLEAR": return { items: [] };
}
}
const CartContext = createContext<{ state: CartState; dispatch: React.Dispatch<CartAction> } | null>(null);
function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
return <CartContext.Provider value={{ state, dispatch }}>{children}</CartContext.Provider>;
}
function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error("useCart must be used within CartProvider");
return ctx;
}
See state-management for:
- Zustand store patterns, Jotai atoms, context splitting
Component Composition Patterns
function Tabs({ children, defaultIndex = 0 }: { children: ReactNode; defaultIndex?: number }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
const tabs = React.Children.toArray(children) as React.ReactElement<{ label: string; children: ReactNode }>[];
return (
<div>
<div role="tablist">
{tabs.map((tab, i) => (
<button key={i} role="tab" aria-selected={i === activeIndex} => setActiveIndex(i)}>
{tab.props.label}
</button>
))}
</div>
<div role="tabpanel">{tabs[activeIndex]?.props.children}</div>
</div>
);
}
See component-patterns for:
- Render props, HOCs, slot patterns, polymorphic components, forwardRef
Performance Optimization
const ExpensiveList = memo(function ExpensiveList({ items }: { items: string[] }) {
return <ul>{items.map((item) => <li key={item}>{item}</li>)}</ul>;
});
const Dashboard = lazy(() => import("./Dashboard"));
function App() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("home");
return (
<div>
<button => startTransition(() => setTab("dashboard"))}>Dashboard</button>
{isPending && <span>Loading...</span>}
<Suspense fallback={<div>Loading...</div>}>
{tab === "dashboard" && <Dashboard />}
</Suspense>
</div>
);
}
See performance-patterns for:
- React.memo deep comparison, virtualization, bundle splitting, profiling
Testing with React Testing Library
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("filters items and calls onSelect when clicked", async () => {
const user = userEvent.setup();
const
render(<Search items={["Apple", "Banana", "Avocado"]} />);
await user.type(screen.getByRole("textbox"), "av");
expect(screen.getByText("Avocado")).toBeInTheDocument();
expect(screen.queryByText("Banana")).not.toBeInTheDocument();
await user.click(screen.getByText("Avocado"));
expect(onSelect).toHaveBeenCalledWith("Avocado");
});
See testing-patterns for:
- Async testing, MSW API mocking, testing hooks, context providers
Anti-Patterns
| Avoid | Use Instead |
|---|---|
useEffect for derived state |
useMemo or compute inline during render |
| Prop drilling through 4+ levels | Context API or external store (Zustand) |
any type for props or state |
Explicit interfaces and generics |
| Index as key for dynamic lists | Stable unique IDs (crypto.randomUUID()) |
| Giant monolith components (300+ lines) | Extract sub-components and custom hooks |
useEffect to sync props to state |
Derive from props directly, use key to reset |
| Memoizing everything by default | Profile first, memoize only proven bottlenecks |
| Testing implementation details | Test rendered output and user interactions |
Fetching in useEffect without cleanup |
Use AbortController or a data-fetching library |
Performance
- Measure first — Use React DevTools Profiler to identify actual bottlenecks before optimizing
- Reduce re-renders — Split context providers, use
memoon expensive subtrees - Code-split aggressively — Lazy-load routes and heavy components with
React.lazy+Suspense - Virtualize long lists — Use
react-windowor@tanstack/react-virtualfor 100+ items - Debounce expensive operations — Wrap search inputs with debounce or
useTransition - Avoid inline objects in JSX —
style={{}}creates new references every render; hoist or memoize
source: React 18 documentation, React TypeScript Cheatsheet, Testing Library docs, Zustand/Jotai docs
Converted and distributed by TomeVault — claim your Tome and manage your conversions.