useRef: The Hook That Remembers Without Re-Rendering
Muhammad Athar

If you've read Make Use of useMemo Like You Mean It and Did You Know useCallback Can Actually Do This? you've now seen two hooks whose job is to remember things between renders — memoized values and memoized functions.
useRef remembers things too. But it works differently in one important way that makes it the right tool for a whole different category of problems.
When useState or useMemo values change, the component re-renders. useRef doesn't do that. It holds a value, that value can change, and the component has no idea. No re-render. No recalculation. Just a box with a value in it that persists for the entire lifetime of the component.
That sounds strange at first. Why would you want to store a value that doesn't trigger a re-render? By the end of this article, you'll have three good answers.
The basic shape
useRef returns an object with a single property: current.
You read the value with .current. You write the value with .current = newValue. That's the entire API.
The object itself — the { current: ... } container — stays the same reference for the entire lifetime of the component. Only the value inside .current changes.
The three jobs useRef does
Job 1: Accessing DOM elements directly
This is the most common use. You need a reference to a real DOM node — to focus an input, measure an element's dimensions, scroll to a position, or integrate with a third-party library that needs a DOM element.
When you pass a ref to the ref prop of a JSX element, React sets ref.current to the actual DOM node when the component mounts. It sets it back to null when the component unmounts.
No library. No document.getElementById. Just a direct reference to the node.
Job 2: Storing values that shouldn't trigger re-renders
Some values need to persist across renders but changing them shouldn't cause the component to update its UI. The classic examples: timer IDs, previous prop values, whether a component has mounted, scroll positions you're tracking.
If this were useState instead of useRef, setting the timer ID would trigger a re-render — which would re-run the effect — which would set a new timer — which would trigger another re-render. useRef stores the timer ID quietly without disrupting the render cycle.
Job 3: Tracking previous values
React doesn't give you the previous value of a prop or state out of the box. useRef is how you build that yourself.
On every render, previousRef.current holds the value from the last render — because the effect that updates it runs after the render, so the ref still has the old value when the render is painting. By the time the next render happens, the effect has run and the ref is updated.
Building something real: an autosaving text editor
Here's a complete autosaving text editor widget that uses useRef for three things simultaneously — a DOM reference for the textarea, a timer reference for debounced saves, and a save count reference for the status display.
Three refs, three different jobs:
textareaRef — a DOM reference. Used once on mount to focus the textarea. textareaRef.current points to the actual <textarea> element in the DOM.
saveTimerRef — stores the timer ID from setTimeout. Changed on every keystroke but never causes a re-render. If it were state, every keystroke would trigger an extra re-render just to store a number the UI doesn't display.
saveCountRef — tracks how many times the document has been saved. Incremented inside an async function without triggering a re-render mid-save. Only displayed after a save completes — so it's read when the status changes to 'saved', not on every increment.
useRef vs useState — when to use each
The decision is straightforward once you know the rule:
Does changing this value need to update the UI?
- Yes →
useState - No →
useRef
A timer ID never appears on screen — useRef. A character count that appears in the toolbar — useState. A previous price value that drives an arrow indicator on screen — depends on the arrow: if the arrow shows, the previous price feeds useState via usePrevious; if it's just internal logic, useRef is enough.
The one thing that trips everyone up
useRef does not trigger a re-render when .current changes. This means if you display ref.current in your JSX directly, it will not update when the ref changes.
useRef is for values the component needs to remember but not display. The moment a value needs to appear on screen, it belongs in state.
Where this leads
useImperativeHandle — lets a parent component call methods on a child component using a ref. The advanced version of DOM refs — used in component libraries when you need to expose focus(), scroll(), or reset() on a component you don't own.
forwardRef — passes a ref through a component to a DOM element inside it. When you build a custom <Input> component and want the parent to be able to focus it, forwardRef is how the ref reaches the underlying <input>.
useCallback with refs — the ref callback pattern, where you pass a function as a ref prop instead of a ref object. Useful for observing when an element mounts or unmounts. Alex touches on this in How to Build a Custom Hook That Actually Earns Its Abstraction.
This article is part of the "From the Builder" series — prompted by the React Foundations articles on DesignDev.io. If the autosaving editor above sparked something, the next natural read is How to Build a Custom Hook That Actually Earns Its Abstraction by Alex Chen — which shows how useRef lives inside a well-designed hook.
Muhammad Athar is the founder and engineer behind DesignDev.io. He writes the "From the Builder" series — concept explainers triggered by the site's own articles, always grounded in something you can actually build.
Photo by Kelly Sikkema on Unsplash

