React

useRef โ€” DOM Access & Persistent Values

Thirdy Gayares
10 min read

๐ŸŽ“ 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
1

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).

2

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.

Note the optional chaining: inputRef.current?.focus(). .current starts as null before the first render completes, so always guard against that.
3

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

Always clean up. Clear the interval both when the user clicks "Stop" and in a useEffect cleanup function, so it doesn't keep running after the component unmounts.
4

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.

This component has rendered 1 time(s) โ€” read from a ref that updates during render but never causes one.
5

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: โ€”

Why this works: the 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.
6

useRef vs. useState

useStateuseRef
Triggers a re-render on change?YesNo
Value persists across renders?YesYes
Read during render?Always currentCurrent, but stale-looking after an update this render
Use for UI that must updateโœ…โŒ
Use for DOM nodes, timers, previous valuesโŒโœ…
7

Common Mistakes

1Expecting a ref update to re-render the UI
mistake-no-rerender.jsx
// โŒ 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>;
2Reading ref.current before it's attached
mistake-null-ref.jsx
// โŒ 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
}, []);
3Using a ref for values the UI must reflect
mistake-wrong-tool.jsx
// โŒ 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)} />;
8

Best Practices

โœ…Use refs for DOM access โ€” focus, scroll, measuring, integrating non-React libraries
โœ…Use refs for values the UI doesn't display โ€” timer IDs, previous values, render counts
โœ…Guard with optional chaining โ€” ref.current?.method() โ€” before the DOM mounts
โœ…Never use a ref for anything the JSX renders โ€” that's what state is for
โœ…Clean up timers/subscriptions stored in refs inside a useEffect cleanup
9

Pattern Reference

PatternUse Case
useRef(null) + ref={...}Access a DOM node directly
useRef(value) mutated in handlersStore a timer ID, a flag, a cache โ€” no re-render needed
useRef + useEffectTrack the previous value of a prop or state
useRef(0), current += 1 in renderCount renders for debugging (dev only)
10

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 setInterval that polls currentTime for the progress bar
  • Uses usePrevious() to detect when playback state changed, and log it
  • Cleans up the interval in a useEffect cleanup when the component unmounts
11

What's Next?

๐Ÿš€ Next Learning Topics:
  • Custom Hooks: extract usePrevious and 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.

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.