D
Dev SOPKnowledge Base
Search
← All topics

React Advanced Hooks, Patterns, and Performance Optimization

Advanced React patterns — custom hooks for async state, useMemo/useCallback optimization, useReducer for complex state, data fetching with SWR/TanStack Query, context with selectors, portals, and concurrent mode patterns.

reacthookstypescriptperformancestate-managementpatterns
Agent trigger phrases: React hooks · custom hook · useMemo · useCallback · useReducer · React performance · React context · React patterns · SWR · TanStack Query

Overview

Advanced React patterns for production applications. Covers state management beyond useState, performance optimization, data fetching, and composable custom hooks.

Custom Hook: Async State

type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[] = []) {
  const [state, setState] = useState<AsyncState<T>>({ status: "idle" });

  useEffect(() => {
    let cancelled = false;
    setState({ status: "loading" });

    asyncFn()
      .then((data) => {
        if (!cancelled) setState({ status: "success", data });
      })
      .catch((error) => {
        if (!cancelled) setState({ status: "error", error: error as Error });
      });

    return () => { cancelled = true; };
  }, deps);

  return state;
}

// Usage
function UserProfile({ userId }: { userId: string }) {
  const state = useAsync(() => fetchUser(userId), [userId]);

  if (state.status === "loading") return <Skeleton />;
  if (state.status === "error") return <ErrorMessage error={state.error} />;
  if (state.status === "success") return <Profile user={state.data} />;
  return null;
}

useReducer for Complex State

type State = {
  items: Item[];
  selectedId: string | null;
  filter: "all" | "active" | "archived";
  isLoading: boolean;
};

type Action =
  | { type: "SET_ITEMS"; items: Item[] }
  | { type: "SELECT"; id: string }
  | { type: "DESELECT" }
  | { type: "SET_FILTER"; filter: State["filter"] }
  | { type: "SET_LOADING"; loading: boolean };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "SET_ITEMS":
      return { ...state, items: action.items, isLoading: false };
    case "SELECT":
      return { ...state, selectedId: action.id };
    case "DESELECT":
      return { ...state, selectedId: null };
    case "SET_FILTER":
      return { ...state, filter: action.filter };
    case "SET_LOADING":
      return { ...state, isLoading: action.loading };
    default:
      return state;
  }
}

function ItemList() {
  const [state, dispatch] = useReducer(reducer, {
    items: [],
    selectedId: null,
    filter: "all",
    isLoading: false,
  });

  const filtered = useMemo(
    () => state.items.filter(item =>
      state.filter === "all" || item.status === state.filter
    ),
    [state.items, state.filter]
  );

  return (/* ... */);
}

useMemo and useCallback: When to Use

// useMemo: expensive computations
const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);  // only recompute when items changes

// useCallback: stable function reference for child components
const handleDelete = useCallback((id: string) => {
  dispatch({ type: "DELETE", id });
}, []);  // stable reference — doesn't cause unnecessary re-renders in children

// When NOT to memoize (premature optimization):
// - Simple value derivations
// - Functions passed to DOM elements (not child components)
// - Values used only once

Context with Selector Pattern

Avoid unnecessary re-renders by splitting context or using selectors:

// Split contexts by update frequency
const UserDataContext = createContext<User | null>(null);
const UserActionsContext = createContext<UserActions | null>(null);

function UserProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const actions = useMemo(() => ({
    updateName: (name: string) => setUser(u => u ? { ...u, name } : u),
    logout: () => setUser(null),
  }), []);

  return (
    <UserActionsContext.Provider value={actions}>
      <UserDataContext.Provider value={user}>
        {children}
      </UserDataContext.Provider>
    </UserActionsContext.Provider>
  );
}

// Consumers only re-render when their specific slice changes
function UserName() {
  const user = useContext(UserDataContext);
  return <span>{user?.name}</span>;
}

function LogoutButton() {
  const { logout } = useContext(UserActionsContext)!;
  return <button onClick={logout}>Logout</button>;  // stable reference = no re-render on user change
}

TanStack Query (Data Fetching)

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function ProjectList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["projects"],
    queryFn: () => fetch("/api/projects").then(r => r.json()),
    staleTime: 5 * 60 * 1000,  // 5 minutes
  });

  const queryClient = useQueryClient();
  const createProject = useMutation({
    mutationFn: (name: string) =>
      fetch("/api/projects", { method: "POST", body: JSON.stringify({ name }) }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["projects"] });
    },
  });

  if (isLoading) return <Spinner />;
  if (error) return <Error />;

  return (
    <div>
      {data.map(p => <ProjectCard key={p.id} project={p} />)}
      <button onClick={() => createProject.mutate("New Project")}>
        Create
      </button>
    </div>
  );
}

Custom Hook: Local Storage State

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setStoredValue = useCallback((newValue: T | ((prev: T) => T)) => {
    setValue(prev => {
      const resolved = typeof newValue === "function"
        ? (newValue as (prev: T) => T)(prev)
        : newValue;
      localStorage.setItem(key, JSON.stringify(resolved));
      return resolved;
    });
  }, [key]);

  return [value, setStoredValue] as const;
}

Custom Hook: Debounced Value

function useDebounce<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);

  return debounced;
}

// Usage: debounce search input
function SearchBox() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  // Only fires 300ms after user stops typing
  useEffect(() => {
    if (debouncedQuery) fetchResults(debouncedQuery);
  }, [debouncedQuery]);
}

Portal Pattern

import { createPortal } from "react-dom";

function Modal({ isOpen, onClose, children }: ModalProps) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.getElementById("modal-root")!
  );
}

Performance Checklist

[ ] Large lists use virtualization (react-window or tanstack-virtual)
[ ] Images lazy-loaded and sized (next/image)
[ ] Bundle split at route level (Next.js default, manual with lazy())
[ ] No anonymous object/array literals in JSX props (creates new ref each render)
[ ] Context value memoized with useMemo
[ ] Event handlers from useCallback when passed to memoized children
[ ] React.memo on components with expensive render and stable props