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.
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
| Smell | Fix |
|---|---|
| Effect runs on every render | Add dependency array; memoize callbacks with useCallback. |
| Flickering values between renders | Use useRef for non-visual mutable values. |
| Slow rerenders from heavy calculations | Wrap with useMemo and ensure stable dependencies. |
| Event handlers recreated unnecessarily | Hoist functions + useCallback to keep referential stability. |
| State updates after unmount | Track 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.