React Hooks Deep Dive: useState to useRef and When to Use Each
A practical deep dive into useState, useEffect, useMemo, useCallback, and useRef. Learn when to reach for each hook and the traps that ruin React code.
What you'll learn
- ✓What state is and is not, and why useState is special
- ✓The real mental model for useEffect
- ✓When useMemo and useCallback help and when they hurt
- ✓What useRef can do beyond DOM access
- ✓Common hook pitfalls and how to avoid them
Prerequisites
- •Basic React: components and JSX
React hooks look simple — five primitives that do most of the work. The trap is that they reward shallow learning and punish it later. Once you ship a real app, you hit subtle bugs around stale closures, unnecessary renders, and effects firing twice. This article goes one level deeper than the docs and tries to answer the question developers actually have: when do I reach for which hook?
useState — the one you cannot mess up too badly
useState returns a value and a setter. The setter triggers a re-render. That is the entire mental model.
const [count, setCount] = useState(0);
function increment() {
setCount(c => c + 1);
}
Two rules that prevent 90% of bugs:
- Use the functional updater when the new state depends on the old:
setCount(c => c + 1). Otherwise you risk stale values from closures. - Do not store derived data. If you can compute it from existing state or props, do not store it. Storing derived data turns into stale-state bugs.
// bad
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // derived!
// good
const [items, setItems] = useState([]);
const count = items.length;
useEffect — the most misunderstood hook
The official model is “useEffect synchronizes your component with an external system.” Repeat that until it sticks.
It is not “useEffect runs after render.” That is the mechanism, not the intent. If you find yourself fighting useEffect, you are usually using it for something it was not designed for.
Good uses:
- Subscribing to a WebSocket or event emitter.
- Setting
document.title. - Fetching data when the component mounts (although a data-fetching library is usually better).
Bad uses:
- Computing derived state — use a regular variable or
useMemo. - Reacting to a user event — handle it in the event handler instead.
- Resetting state when a prop changes — pass a
keyto the component, do not chain effects.
The classic shape:
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
Two non-negotiables:
- Return a cleanup function when the effect creates anything that needs tearing down.
- List every value you use in the dependency array. The lint rule is right; do not silence it.
Stale closures
The single biggest source of useEffect bugs is the stale closure. The function inside the effect captures the values from the render it was created in. If you read state without listing it as a dependency, you read the wrong value forever.
Fix: list the dependency, or use the functional updater so you do not need to read the value at all.
useMemo — the hook you probably use too much
useMemo caches the result of a computation between renders, recomputing only when its dependencies change.
const sorted = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
Use it when:
- The computation is genuinely expensive (sorting a 10k-item list, parsing a complex object).
- You need a stable object identity to pass to a child wrapped in
React.memoor to a dependency array.
Do not use it for:
- Cheap computations. The memo bookkeeping costs more than the work.
- Premature optimization. Profile first.
Rule of thumb: if you removed every useMemo and the app felt the same, they were all noise.
useCallback — useMemo’s sibling
useCallback is just useMemo for functions. It returns the same function reference between renders, unless dependencies change.
const onSelect = useCallback(
(id: string) => setSelectedId(id),
[]
);
Same rules: use it only when the stable reference matters to a child or an effect’s dependency array. A useCallback whose only consumer is a regular DOM handler is wasted code.
A real pattern where it pays off: an effect that subscribes to a service which takes a callback. Without useCallback, the effect re-runs every render because the callback identity changes.
useRef — more than just DOM access
useRef returns a mutable container whose .current property persists across renders without triggering re-renders.
The textbook use is grabbing a DOM node:
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
But the powerful use is carrying mutable state that should not trigger re-renders:
- Holding the latest value of a prop or state for use inside a long-lived callback.
- Tracking whether a component is mounted (though React Strict Mode complicates this).
- Storing timer IDs, AbortControllers, observers.
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value;
});
const onTick = useCallback(() => {
console.log('latest', latestValue.current);
}, []);
This is the latest ref pattern — a clean way to read fresh values inside callbacks you do not want to rebuild.
A decision matrix
What do I need? Reach for
A value that triggers re-render on change useState
A side effect tied to a value's lifecycle useEffect
A cached, expensive computation useMemo
A stable function reference useCallback
A mutable holder that does not re-render useRef
If none of those match, you probably do not need a hook at all — a plain variable inside the render does the job.
The big traps
- Conditional hooks: never call a hook inside an
if. Hooks rely on call order. - Effects depending on objects: an object literal in the dependency array is a new reference every render. Move the object out, memoize it, or depend on its primitive parts.
- Forgetting cleanup: leaks pile up silently and surface as memory bloat in production.
- Treating useEffect as a lifecycle method: it is not
componentDidMount. Strict Mode runs it twice in development to remind you.
Beyond the basics
Once you understand the core five, learn useReducer (for complex state), useContext (for global-ish values), useId (for stable IDs in SSR), and useSyncExternalStore (for subscribing to non-React stores). Then read the official docs on useTransition and useDeferredValue for the concurrent-mode story.
The hooks API rewards investment. Each concept you master removes a category of bugs from your future code. Start with the five here, get comfortable, and the rest will fit naturally.
Related articles
- JavaScript React Custom Hooks Patterns
Learn powerful patterns for building React custom hooks — extracting logic, composing hooks, handling cleanup, and designing clean APIs your team will love.
- JavaScript React useEffect Common Mistakes
Avoid the most common useEffect mistakes — missing dependencies, infinite loops, stale closures, and effects that should not be effects in the first place.
- JavaScript React Context API Guide
Learn how to use React's Context API to share state across your component tree without prop drilling — with patterns, performance tips, and common mistakes to avoid.
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.