intermediate
frontend

Hooks Deep Dive

Custom hooks, useEffect cleanup, useMemo, useCallback, and performance hooks

react fundamentals
35 min read

Hooks Deep Dive

Hooks are the primitives that let function components hold state, run side effects, and share logic. useState and useEffect get you started, but real applications live or die on the details: cleaning up subscriptions, keeping dependency arrays honest, avoiding needless recomputation, and packaging repeated logic into custom hooks. This lesson digs into the intermediate hooks you reach for once components grow past a single input and a click handler — the tools that keep an app fast, predictable, and easy to maintain.

Learning Objectives
  • Write correct cleanup functions in useEffect to avoid leaks
  • Reason precisely about the dependency array
  • Memoize expensive work with useMemo and stabilize functions with useCallback
  • Extract reusable stateful logic into custom hooks
  • Recognize when memoization helps versus when it just adds noise

useEffect and Cleanup

useEffect runs a side effect after the browser paints. Effects that create ongoing resources — timers, subscriptions, event listeners, network streams — must return a cleanup function. React calls that cleanup before re-running the effect and again when the component unmounts. Without it you leak listeners and end up with stale callbacks firing after the component is gone.

tsx
import { useEffect, useState } from "react" function WindowWidth() { const [width, setWidth] = useState(window.innerWidth) useEffect(() => { const onResize = () => setWidth(window.innerWidth) window.addEventListener("resize", onResize) // Cleanup runs before the next effect and on unmount return () => window.removeEventListener("resize", onResize) }, []) // empty array: subscribe once return <p>Window is {width}px wide</p> }

For data fetching, cleanup also lets you ignore results from a request that has been superseded — the classic "race condition on the last keystroke" bug:

tsx
useEffect(() => { let active = true const controller = new AbortController() fetch(`/api/users/${id}`, { signal: controller.signal }) .then((res) => res.json()) .then((data) => { if (active) setUser(data) // ignore if a newer effect already ran }) .catch((err) => { if (err.name !== "AbortError") throw err }) return () => { active = false controller.abort() } }, [id])

The Dependency Array

The dependency array tells React when to re-run an effect. React compares each item with Object.is against the previous render. If nothing changed, the effect is skipped. The rule is simple to state and easy to violate: every reactive value used inside the effect must be listed — props, state, and any function or variable derived from them.

tsx
useEffect(() => { const id = setInterval(() => { console.log(`Room ${roomId}, user ${userId}`) }, 1000) return () => clearInterval(id) }, [roomId, userId]) // both are read inside, so both belong here
Runs once after the first render. Use for one-time setup like subscriptions that never depend on props.

useMemo: Cache Expensive Values

useMemo(fn, deps) remembers the return value of fn and recomputes only when a dependency changes. Use it for genuinely expensive calculations, or to keep a referentially stable object/array so downstream memo components and effect dependencies don't churn.

tsx
import { useMemo } from "react" function ProductList({ products, query }) { const filtered = useMemo(() => { return products .filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) .sort((a, b) => a.price - b.price) }, [products, query]) // recompute only when inputs change return ( <ul> {filtered.map((p) => ( <li key={p.id}>{p.name}</li> ))} </ul> ) }

useCallback: Stabilize Function Identity

Every render creates new function instances. useCallback(fn, deps) returns the same function reference between renders until a dependency changes. It matters when a function is passed to a memo-wrapped child or used as an effect dependency — a fresh reference each render would defeat the memoization or retrigger the effect.

tsx
import { useCallback, memo, useState } from "react" const Row = memo(function Row({ id, onSelect }) { console.log("render", id) return <button onClick={() => onSelect(id)}>Select {id}</button> }) function Table({ rows }) { const [selected, setSelected] = useState(null) // Stable reference: Row stays memoized across re-renders const handleSelect = useCallback((id) => setSelected(id), []) return rows.map((r) => <Row key={r.id} id={r.id} onSelect={handleSelect} />) }

useCallback(fn, deps) is exactly useMemo(() => fn, deps) — the same cache, specialized for functions. If the child is not memoized and the function is not an effect dependency, wrapping it adds no benefit.

Custom Hooks: Reusable Logic

A custom hook is any function whose name starts with use and which calls other hooks. It lets you extract stateful logic so multiple components share behavior — not state. Each component that calls the hook gets its own isolated state.

tsx
import { useState, useEffect } from "react" function useDebounce<T>(value: T, delay = 300): T { const [debounced, setDebounced] = useState(value) useEffect(() => { const id = setTimeout(() => setDebounced(value), delay) return () => clearTimeout(id) // cancel if value changes before delay }, [value, delay]) return debounced } // Usage: only fires the search after the user stops typing function Search() { const [term, setTerm] = useState("") const debouncedTerm = useDebounce(term, 400) useEffect(() => { if (debouncedTerm) fetch(`/api/search?q=${debouncedTerm}`) }, [debouncedTerm]) return <input value={term} onChange={(e) => setTerm(e.target.value)} /> }

A second common example — reading and syncing to localStorage:

tsx
function useLocalStorage<T>(key: string, initial: T) { const [value, setValue] = useState<T>(() => { const stored = localStorage.getItem(key) return stored ? (JSON.parse(stored) as T) : initial }) useEffect(() => { localStorage.setItem(key, JSON.stringify(value)) }, [key, value]) return [value, setValue] as const }

Common Pitfalls


Next Steps

Continue Learning
Client-side routing and protected routes.

Next Lesson

React Router