Module 4: Hooks and Lifecycles
useRef: Mutable Values and Direct DOM Access
What a ref is, when it's the right tool instead of state, how to reach DOM nodes safely, and the changes to refs in React 19.
Sometimes you need to hold onto something between renders without telling React to render again. A timer id. The previous value of a prop. A button you want to focus. State isn't the right place for those, because changing state triggers a render, and here you don't want one. That's the gap useRef fills.
What a ref is#
const ref = useRef(0);
console.log(ref.current); // 0
ref.current = 5; // no re-render
useRef(initial) returns a plain object, { current: initial }, and React gives you the same object on every render. You can read and write .current freely. React doesn't watch it.
useState useRef
──────── ──────
persists across renders persists across renders
changing it triggers render changing it does NOT trigger render
snapshot per render one shared mutable box
use for: things shown in UI use for: things behind the scenes
The question I ask: "does the screen need to change when this value changes?" If yes, it's state. If no, it may well be a ref.
One rule: not during render#
Don't read or write ref.current in the body of the component while rendering. That would make render output depend on something React can't track, and breaks the purity rule. Read and write refs in event handlers and effects.
Storing values that aren't UI#
A classic example is a stopwatch, where the interval id is needed to stop it but nobody needs to see the id.
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => stop, []); // clear on unmount
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
If you stored intervalRef in state instead, every start would cause a pointless render, and worse, a stale closure might hold an old id.
Another good use is remembering the previous value:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // runs after render, so during render ref.current is still the old value
});
return ref.current;
}
Reaching DOM nodes#
Give an element a ref, and React sets ref.current to the actual DOM node once it's mounted.
function SearchBar() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} placeholder="Search" />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
Between creating the component and mounting it, inputRef.current is null. That's why reading it during render fails. After mount, it's the <input> element. When the element unmounts, React sets it back to null.
Things you legitimately need refs for: focusing, scrolling an element into view, measuring size with getBoundingClientRect(), playing and pausing media, and handing a DOM node to a non-React library.
function Chat({ messages }) {
const bottomRef = useRef(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div className="chat">
{messages.map((m) => <p key={m.id}>{m.text}</p>)}
<div ref={bottomRef} />
</div>
);
}
What you should not do is use refs to change what's on the screen. Setting inputRef.current.value = "x" or changing textContent by hand fights with React. It will happily overwrite your change on the next render. Let state and props drive the UI, and use refs for the imperative stuff that React doesn't model, like focus.
Refs in React 19: ref as a prop#
To give a parent access to a child's DOM node, in older React you needed forwardRef. In React 19, ref is just another prop for function components:
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function Form() {
const nameRef = useRef(null);
return (
<>
<TextInput ref={nameRef} placeholder="Name" />
<button onClick={() => nameRef.current.focus()}>Focus name</button>
</>
);
}
forwardRef still works, and you'll find it in lots of existing code and libraries, so don't be thrown when you see it. But new code doesn't need it.
Two other 19 details. A ref callback can now return a cleanup function, which React runs when the element detaches:
<div
ref={(node) => {
const observer = new ResizeObserver(() => { /* ... */ });
observer.observe(node);
return () => observer.disconnect();
}}
/>
And React doesn't expose a child's internals by default. If you want a parent to call specific methods on a child (like .reset()), use useImperativeHandle to expose a deliberately small API, rather than handing over the whole DOM node. Treat that as rare.
Refs for lists#
You can't call useRef in a loop. For a list of nodes, use one ref that holds a Map, or a ref callback:
const itemRefs = useRef(new Map());
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
itemRefs.current.set(item.id, node);
return () => itemRefs.current.delete(item.id);
}}
>
{item.name}
</li>
))}
Where people get burned#
- Using a ref where the UI should update, then wondering why the screen doesn't change.
- Reading
ref.currentduring render and gettingnullon the first pass. - Mutating DOM that React also controls, so React later undoes the change.
- Storing something in
useRefto "avoid re-renders" when it really is state that should be shown. - Forgetting that
ref.currentcan benulland calling.focus()on it unguarded.
Practice#
- Build an input that focuses itself when the component mounts, using a ref and an effect.
- Make a form with a "Focus the email field" button, using the React 19 ref-as-prop pattern on a custom
TextInputcomponent. - Create a counter that shows how many times a component has rendered, using a ref (hint: increment inside an effect). Why is this a bad fit for state?
- Write a
usePrevioushook and use it to show "was 4, now 7" under a number that you can increment.