React Expert
React 19 solidifies the declarative, server-centric direction of modern React with new hooks,
improved server component semantics, and a compiler that makes many manual memoization
decisions obsolete. Understanding when NOT to use memoization, how reconciliation actually
works, and how to structure components for composability separates expert React from intermediate.
Core Mental Model
React renders are cheap; unnecessary ones cost you mainly in predictability and profiler noise,
not usually in real-world performance. Before reaching for useMemo/useCallback, measure.
Design components around data flow: lift state only as high as needed, co-locate state with the
component that owns it, and split contexts by update frequency. Custom hooks are your primary
abstraction tool — they let you test logic independently of UI.
Custom Hooks for Logic Extraction
useDebounce
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage: search input that waits for user to stop typing
function AgentSearch() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const { data } = useQuery(["agents", debouncedQuery], () => searchAgents(debouncedQuery), {
enabled: debouncedQuery.length > 1,
});
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
useLocalStorage with SSR safety
import { useState, useCallback } from "react";
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
setStoredValue(prev => {
const next = value instanceof Function ? value(prev) : value;
try { window.localStorage.setItem(key, JSON.stringify(next)); } catch {}
return next;
});
}, [key]);
return [storedValue, setValue] as const;
}
useFetch with AbortController
import { useState, useEffect, useRef } from "react";
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({ data: null, loading: true, error: null });
useEffect(() => {
const controller = new AbortController();
setState(s => ({ ...s, loading: true }));
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => {
if (error.name !== "AbortError") {
setState({ data: null, loading: false, error });
}
});
return () => controller.abort();
}, [url]);
return state;
}
useMemo and useCallback — When They Actually Help
// ✅ useMemo: expensive pure computation on large data
const sortedAgents = useMemo(
() => agents.slice().sort((a, b) => a.displayName.localeCompare(b.displayName)),
[agents] // only re-sort when agents changes
);
// ✅ useCallback: function passed to memoized child component
const handleSelect = useCallback((id: string) => {
dispatch({ type: "SELECT_AGENT", payload: id });
}, [dispatch]); // dispatch from useReducer is stable
// ❌ useMemo on trivial computation
const title = useMemo(() => `Hello ${name}`, [name]); // waste — just compute inline
// ❌ useCallback without memoized child
const handler = useCallback(() => doThing(), []); // only helps if passed to React.memo child
// ✅ React.memo prevents child re-render when props are the same
const AgentCard = React.memo<{ agent: Agent; onSelect: (id: string) => void }>(
({ agent, onSelect }) => (
<div onClick={() => onSelect(agent.id)}>{agent.displayName}</div>
)
);
useReducer for Complex State
type AgentState = {
agents: Agent[];
selected: string | null;
filter: string;
loading: boolean;
};
type AgentAction =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: Agent[] }
| { type: "FETCH_ERROR"; payload: Error }
| { type: "SELECT"; payload: string }
| { type: "SET_FILTER"; payload: string };
function agentReducer(state: AgentState, action: AgentAction): AgentState {
switch (action.type) {
case "FETCH_START": return { ...state, loading: true };
case "FETCH_SUCCESS": return { ...state, loading: false, agents: action.payload };
case "SELECT": return { ...state, selected: action.payload };
case "SET_FILTER": return { ...state, filter: action.payload };
default: return state;
}
}
function AgentDashboard() {
const [state, dispatch] = useReducer(agentReducer, {
agents: [], selected: null, filter: "", loading: false,
});
// State transitions are predictable and testable
useEffect(() => {
dispatch({ type: "FETCH_START" });
fetchAgents().then(agents => dispatch({ type: "FETCH_SUCCESS", payload: agents }));
}, []);
}
useRef — DOM and Mutable Values
import { useRef, useEffect, useCallback } from "react";
// Ref for DOM access
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
// Ref for mutable value that doesn't trigger re-render
function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef(callback);
// Store latest callback without re-creating the interval
useEffect(() => { savedCallback.current = callback; }, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
// Ref for tracking mounted state (avoid setState after unmount)
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false; };
}, []);
return isMounted;
}
Context with Performance (Splitting Contexts)
// ❌ One big context: every consumer re-renders on any state change
const AppContext = createContext<{ user: User; theme: Theme; cart: CartItem[] }>(null!);
// ✅ Split by update frequency
const UserContext = createContext<User>(null!); // changes rarely
const ThemeContext = createContext<Theme>(null!); // changes rarely
const CartContext = createContext<CartItem[]>(null!); // changes often
// ✅ Separate state and dispatch contexts
const AgentStateContext = createContext<AgentState>(null!);
const AgentDispatchContext = createContext<Dispatch<AgentAction>>(null!);
export function AgentProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(agentReducer, initialState);
// Consumers that only dispatch won't re-render on state changes
return (
<AgentStateContext.Provider value={state}>
<AgentDispatchContext.Provider value={dispatch}>
{children}
</AgentDispatchContext.Provider>
</AgentStateContext.Provider>
);
}
Compound Component Pattern
// Flexible API: <Tabs> <Tabs.List> <Tabs.Tab> <Tabs.Panel>
import { createContext, useContext, useState } from "react";
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue>(null!);
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }: { children: React.ReactNode }) {
return <div role="tablist">{children}</div>;
};
Tabs.Tab = function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
Tabs.Panel = function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
};
// Usage
<Tabs defaultTab="profile">
<Tabs.List>
<Tabs.Tab id="profile">Profile</Tabs.Tab>
<Tabs.Tab id="skills">Skills</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="profile"><ProfileContent /></Tabs.Panel>
<Tabs.Panel id="skills"><SkillsContent /></Tabs.Panel>
</Tabs>
React 19 Features
useOptimistic — Optimistic Updates
import { useOptimistic, useTransition } from "react";
function AgentSkillList({ skills, agentId }: Props) {
const [optimisticSkills, addOptimisticSkill] = useOptimistic(
skills,
(current, newSkill: Skill) => [...current, { ...newSkill, pending: true }]
);
async function handleAddSkill(formData: FormData) {
const skill = { id: crypto.randomUUID(), name: formData.get("name") as string };
addOptimisticSkill(skill); // immediately show in UI
await addSkill(agentId, skill); // server mutation (may take 200ms)
}
return (
<form action={handleAddSkill}>
{optimisticSkills.map(s => (
<div key={s.id} style={{ opacity: s.pending ? 0.6 : 1 }}>{s.name}</div>
))}
<input name="name" required />
<button type="submit">Add Skill</button>
</form>
);
}
use() Hook — Promises and Context
import { use, Suspense } from "react";
// use() can unwrap promises inside components (must be in Suspense)
function AgentDetails({ agentPromise }: { agentPromise: Promise<Agent> }) {
const agent = use(agentPromise); // suspends until resolved
return <div>{agent.displayName}</div>;
}
// use() reads context conditionally (unlike useContext)
function ConditionalTheme({ dark }: { dark: boolean }) {
if (!dark) return <div>Light mode</div>;
const theme = use(ThemeContext); // OK: called after condition
return <div style={{ background: theme.bg }}>Dark mode</div>;
}
Error Boundaries
import { Component, type ReactNode } from "react";
interface Props { children: ReactNode; fallback: ReactNode; }
interface State { hasError: boolean; error: Error | null; }
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: { componentStack: string }) {
console.error("React error boundary caught:", error, info.componentStack);
// Send to error tracking service
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
// Usage with reset
function AgentDashboard() {
return (
<ErrorBoundary fallback={<div>Something went wrong. <button onClick={() => location.reload()}>Retry</button></div>}>
<AgentList />
</ErrorBoundary>
);
}
Key Prop and Reconciliation
// Keys must be stable, unique identifiers — NOT array index for dynamic lists
// ❌ Index as key causes bugs on reorder/insert/delete
{agents.map((agent, index) => <AgentCard key={index} agent={agent} />)}
// ✅ Stable ID from data
{agents.map(agent => <AgentCard key={agent.id} agent={agent} />)}
// ✅ Key as reset mechanism: change key to force full remount
function AgentForm({ agentId }: { agentId: string }) {
return (
// When agentId changes, form fully remounts (no stale state)
<form key={agentId}>
<AgentFields />
</form>
);
}
Suspense and Lazy Loading
import { lazy, Suspense } from "react";
// Code-split heavy components
const AgentAnalytics = lazy(() => import("./AgentAnalytics"));
const AgentSettings = lazy(() => import("./AgentSettings"));
function AgentDashboard() {
return (
<Suspense fallback={<Skeleton />}>
<AgentAnalytics />
</Suspense>
);
}
Anti-Patterns
// ❌ Deriving state from props with useState (stale on prop change)
function BadComponent({ items }) {
const [filtered, setFiltered] = useState(items.filter(x => x.active));
// filtered won't update when items prop changes!
}
// ✅ Derive during render or use useMemo
const filtered = useMemo(() => items.filter(x => x.active), [items]);
// ❌ useEffect for data transformation (should happen during render)
useEffect(() => { setFullName(first + " " + last); }, [first, last]);
// ✅
const fullName = `${first} ${last}`;
// ❌ Prop drilling 5+ levels deep
// ✅ Context or state management library
// ❌ Direct DOM manipulation
document.getElementById("agent-name").textContent = name;
// ✅ State and refs
// ❌ Missing dependency array (runs on every render)
useEffect(() => { fetchData(); }); // missing []
Quick Reference
Custom hooks: extract logic + effects, test independently of UI
useMemo: expensive pure computation, NOT trivial expressions
useCallback: stable refs for memoized children (React.memo)
useReducer: 3+ related state fields or complex transitions
useRef: DOM access, mutable values that don't trigger renders
Context split: separate state/dispatch contexts, split by update frequency
Compound: parent context + child sub-components with Tabs/Tab/Panel pattern
React 19: useOptimistic (instant UI), useFormStatus (pending state), use() (promises)
Error boundaries: class component, getDerivedStateFromError + componentDidCatch
Keys: stable IDs from data, not index; change key to force remountSkill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen