Skip to main content

React Hooks in Production: Patterns that Survive Incidents

Incident-tested patterns for useEffect, useMemo, useRef, and custom hooks so your UI stays predictable under load.

SMSarah Miller

Hooks That Don’t Wake You at 2 AM

This isn’t another primer—it is a set of production notes from late-night incidents where a stray useEffect or missing dependency crashed throughput. The goal: patterns you can hand to new teammates and trust.

“If it can rerender, it will. If it can re-run an effect, it will. Make peace with that.”

Effect Hygiene Checklist

  • Declare all dependencies (or intentionally suppress with a comment and reason).
  • Keep effects pure; isolate async work in a function you call inside the effect.
  • Return a cleanup for everything you subscribe to.
  • Avoid mixing data fetching and state derivation in a single effect.

useEffect(() => setState(expensiveCalc()), []) is a silent footgun. Compute derived state inline or memoize it instead.

A Safe Fetch Pattern

function useResource<T>(key: string, fetcher: () => Promise<T>) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);
  const abortRef = useRef<AbortController | null>(null);
 
  useEffect(() => {
    abortRef.current?.abort();
    const abort = new AbortController();
    abortRef.current = abort;
    setLoading(true);
    setError(null);
 
    fetcher()
      .then((res) => !abort.signal.aborted && setData(res))
      .catch((err) => !abort.signal.aborted && setError(err))
      .finally(() => !abort.signal.aborted && setLoading(false));
 
    return () => abort.abort();
  }, [key, fetcher]); // key guards stale fetches
 
  return { data, error, loading };
}

Smell → Fix

SmellFix
Effect runs on every renderAdd dependency array; memoize callbacks with useCallback.
Flickering values between rendersUse useRef for non-visual mutable values.
Slow rerenders from heavy calculationsWrap with useMemo and ensure stable dependencies.
Event handlers recreated unnecessarilyHoist functions + useCallback to keep referential stability.
State updates after unmountTrack abort/cancel; clean up in effect return.

When to Reach for Custom Hooks

Custom hooks are a boundary: once logic touches more than one component or mixes concerns (data + tracking + UI), extract it.

export function useStepTracker(step: string) {
  const start = useRef(performance.now());
 
  useEffect(() => {
    posthog?.capture("step_viewed", { step, at: Date.now() });
    return () => {
      const duration = Math.round(performance.now() - start.current);
      posthog?.capture("step_duration_ms", { step, duration });
    };
  }, [step]);
}

Use it like:

function Checkout() {
  useStepTracker("checkout");
  return <CheckoutForm />;
}

Keep a Runbook

  • Start with dependencies: “Why is this effect re-running?”
  • Check cancellation: “Do we abort pending async work?”
  • Verify memoization: “Is the prop churn self-inflicted?”
  • Add logging: “Can we see effect mount/unmount in devtools?”

The payoff is boring, predictable renders—and one less page in your incident report.