useRef: mutable values & DOM refs
- Explain that useRef returns the same mutable box every render and writing .current never re-renders
- Choose a ref or state for a value by asking whether a change to it should update the screen
- Spot a ref read or written during render, and move it into an event handler or effect
- Predict that a DOM ref is null during the first render and is set only after React commits
- Write a DOM ref that moves focus after an action, where
autoFocus(focus on mount) can't reach
Read
A ref is a box whose .current survives re-renders
FlaguseRef(initial) gives you a plain object { current: initial }. React keeps the
same object across every render, so ref.current is a stable place to stash a
value — and changing it does not trigger a re-render.
function LikeButton() {
const clicks = useRef(0);
function handleClick() {
clicks.current += 1; // ✅ written in an event handler: no re-render
sendAnalytics("like", clicks.current);
}
return <button onClick={handleClick}>Like</button>;
}
Contrast with a normal let clicks = 0 inside the component: that resets to 0 on
every render. ref.current persists. Contrast with useState: state also persists,
but writing to it schedules a render. A ref writes silently.
Reach for a ref when you need to remember something between renders but the UI doesn't need to react to it — a timer id, a previous value, a "did this already run" flag.
Ref vs state — does this change need to re-render?
FlagState and refs both persist across renders. The one question that decides between them: when this value changes, should the screen update?
- Yes →
useState. The value is shown, or drives what's shown. Writing it re-renders. - No →
useRef. The value is bookkeeping the render doesn't read. Writing it is silent.
function Stopwatch() {
const [elapsed, setElapsed] = useState(0); // shown → state
const intervalId = useRef(null); // plumbing → ref
function start() {
clearInterval(intervalId.current); // a second Start must not leave the first one running
intervalId.current = setInterval(() => setElapsed((e) => e + 1), 1000);
}
function stop() {
clearInterval(intervalId.current); // read it back later, no render needed
}
return (
<>
<p>{elapsed}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
The elapsed time is on screen, so it's state. The interval id is only there so
stop() can clear it — nothing renders it — so it's a ref. Putting the id in state
would cause a pointless re-render; putting elapsed in a ref would freeze the
display, since mutating .current never tells React to repaint.
Reading the latest value from a long-lived callback
FlagA callback created in one render closes over that render's variables. If it runs much
later — inside a long-lived interval, a subscription, an event listener you attach
once — it sees the old values. Two bad options come first to mind: add onMessage to the
effect's deps and reconnect the socket every time the parent passes a new function, or
leave it out and call a stale onMessage forever.
Since React 19.2, the tool for this is useEffectEvent. An Effect Event always sees
the latest props and state, and it is not a dependency:
function ChatRoom({ roomId, onMessage }) {
const onReceive = useEffectEvent((m) => onMessage(m)); // always the newest onMessage
useEffect(() => {
const socket = connect(roomId);
socket.on("message", (m) => onReceive(m));
return () => socket.close();
}, [roomId]); // ✅ reconnects for a new room, not for a new onMessage
}
Effect Events have rules: call them only from inside effects (including callbacks the
effect sets up, like this listener), never during render, and never pass them to other
components or hooks. Don't use one to drop a dependency the effect really should react to:
roomId stays in the array.
Quiz
What does useRef(0) return, and what happens when you write to it?
FlagYou need a value that persists across renders. When should it be a ref instead of state?
FlagYou attach a ref to an <input> to call .focus(). Where is it safe to read inputRef.current?
Which of these focus jobs needs a DOM ref, because autoFocus can't do it?
A component shows how often it has rendered with renders.current += 1 in its body. In development under StrictMode (React 19.2, which this course runs), the very first mount already shows 2. Why?
Fill in the blank
Clicking Reply should put the cursor in the comment box: declare a ref, attach it, then focus it from the click handler.
Flagconst boxRef = (null); function handleReply() { boxRef..focus(); } return ( <> <button onClick={handleReply}>Reply</button> <textarea ={boxRef} /> </> );
Store an interval id without causing a re-render, then read it back to clear it.
Flagconst id = useRef(null); id. = setInterval(tick, 1000); // later: clearInterval(id.);
Predict the output
Challenge
Put focus back in the search box after Clear
FlagSearchForm has a search box and a Clear button. Clear empties the box, but focus
stays on the button: the user has to click back into the input before typing the next
search.
React's one declarative focus prop, autoFocus, focuses an element when it mounts.
This input is already mounted when Clear is clicked, so moving focus in response to an
action means calling .focus() on the real DOM node — and reaching a component's own DOM
node is what a ref is for.
Your task
Edit SearchForm.tsx so the tests pass:
- The input renders with its
Search…placeholder and keeps what the user types - Clear empties the input
- Clear moves focus back into the input, even though clicking the button had focused the button — and even when the box was already empty
- With two
SearchForms on the page, each Clear focuses its own form's input