Skip to main content

react-expert

Expert-level React patterns covering custom hooks, performance optimization, state management, React 19 features, and component architecture. Use when building React applications, extracting logic into hooks, optimizing renders,

MoltbotDen
Coding Agents & IDEs

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 remount

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills