๐ What You Will Learn
- DOM access: reach a real DOM node with
refโ focus, scroll, measure - Persistent values: store a value across renders without triggering one
- The interval/timer pattern: holding a timer ID in a ref instead of state
- Tracking previous values: the
usePrevious()custom hook - useRef vs useState: a clear rule for which one to reach for
- Common mistakes: reading refs during render, expecting them to trigger updates
What useRef Actually Gives You
useRef(initialValue) returns a plain object: { current: initialValue }. That object is the same object across every render of the component โ React never recreates it. Two things follow from that: you can mutate .current freely, and doing so does not trigger a re-render.
That makes useRef useful for exactly two jobs: holding a reference to a real DOM node, and holding any other mutable value you need to persist but don't want to cause renders (a timer ID, a previous value, a flag).
Accessing the DOM Directly
Pass a ref to an element's ref prop, and React fills in .current with the actual DOM node once it mounts. From there you can call any native DOM method โ .focus(), .scrollIntoView(), read .value.
inputRef.current?.focus(). .current starts as null before the first render completes, so always guard against that.Persistent Values That Don't Trigger Renders
A classic case: storing a setInterval ID so you can clear it later. If you put that ID in useState, every tick where you save it would trigger an unnecessary re-render. A ref holds it silently.
0s
useEffect cleanup function, so it doesn't keep running after the component unmounts.Mutating .current Doesn't Re-render
This is the core mental model to internalize: changing ref.current is invisible to React. The component only re-renders because of clicks (a real state update) โ the render count itself is just along for the ride.
Tracking a Previous Value
A common need: comparing the current value of a prop or state to what it was last render. There's no built-in usePrevious hook, but it's a perfect fit for useRef + useEffect.
Now: 0 ยท Previous: โ
useEffect runs after the render, so during render ref.current still holds last render's value. Only after painting does the effect update it to the new one.useRef vs. useState
| useState | useRef | |
|---|---|---|
| Triggers a re-render on change? | Yes | No |
| Value persists across renders? | Yes | Yes |
| Read during render? | Always current | Current, but stale-looking after an update this render |
| Use for UI that must update | โ | โ |
| Use for DOM nodes, timers, previous values | โ | โ |
Common Mistakes
// โ BAD: the displayed count never updates โ mutating a ref doesn't re-render
const countRef = useRef(0);
const increment = () => { countRef.current += 1; };
return <button onClick={increment}>{countRef.current}</button>;
// โ
GOOD: use state for anything that should show up on screen
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;// โ BAD: .current is null on first render, before the DOM node exists
useEffect(() => {
inputRef.current.focus(); // may crash if this runs too early
}, []);
// โ
GOOD: guard with optional chaining, or trust useEffect's timing
useEffect(() => {
inputRef.current?.focus(); // runs after mount, .current is set by then
}, []);// โ BAD: form input value stored in a ref โ typing shows nothing on screen
const nameRef = useRef("");
return <input onChange={(e) => (nameRef.current = e.target.value)} />;
// โ
GOOD: controlled inputs need state, not a ref
const [name, setName] = useState("");
return <input value={name} onChange={(e) => setName(e.target.value)} />;Best Practices
ref.current?.method() โ before the DOM mountsuseEffect cleanupPattern Reference
| Pattern | Use Case |
|---|---|
| useRef(null) + ref={...} | Access a DOM node directly |
| useRef(value) mutated in handlers | Store a timer ID, a flag, a cache โ no re-render needed |
| useRef + useEffect | Track the previous value of a prop or state |
| useRef(0), current += 1 in render | Count renders for debugging (dev only) |
Practice Project
Build a small video player control bar that:
- Uses a ref to hold the
<video>element and calls.play()/.pause()directly - Uses a ref to store a
setIntervalthat pollscurrentTimefor the progress bar - Uses
usePrevious()to detect when playback state changed, and log it - Cleans up the interval in a
useEffectcleanup when the component unmounts
What's Next?
- Custom Hooks: extract
usePreviousand similar patterns into a shared library - useEffect: revisit cleanup timing now that refs are in the picture
- useMemo / React.memo: the other tools for controlling what causes a render
- React Hooks Deep Dive: see how all the hooks fit together
Keep practicing! ๐ช Once "does this need to re-render the UI?" becomes your instinct for choosing between useState and useRef, a whole category of unnecessary re-renders disappears from your components.