“What is the difference between useRef and useState, and when would you pick each?” It sounds basic, yet the follow-ups go deep: why does a counter stored in a ref not update the screen, why is reading a ref during render discouraged, and how does a ref help with an interval that keeps seeing stale values?
Interviewers ask because the choice reveals whether you understand what makes React re-render. Choosing wrongly leads either to a UI that silently stops updating or to needless renders on every mouse move.
Before you start
You should know useState, how a component re-renders, and the idea that each render sees its own snapshot of state. You should have attached a ref to a DOM element at least once. This works the same in React 18 and 19; React 19 additionally lets function components receive ref as a normal prop (no forwardRef needed) and lets ref callbacks return a cleanup function.
The short answer
Both persist a value across renders. Setting state tells React the UI may need to change, so it re-renders with the new value. A ref is a plain mutable object, { current: value }, that React gives back unchanged on every render; assigning to ref.current does not re-render anything. Use state for values that appear in the output. Use a ref for values only event handlers and effects need, such as a timer id, a DOM node, the previous value of something, or an imperative handle to a third-party object. Do not read or write ref.current during rendering, except to lazily initialise it.
How it works
Conceptually, useRef is useState with an object you are allowed to mutate and a setter you never call:
function useRefModel(initial) {
const [ref] = useState(() => ({ current: initial }));
return ref;
}The object identity is stable for the component’s lifetime. Mutating its current property is invisible to React. Compare these two counters:
function StateCounter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
function RefCounter() {
const count = useRef(0);
return <button onClick={() => { count.current += 1; }}>{count.current}</button>;
}StateCounter updates on every click. RefCounter increments the number internally, but the button keeps showing 0 until something else causes a re-render, at which point it jumps to the hidden value. That jump is why reading refs in render is fragile: the output depends on when a render happens to occur.
Step-by-step walkthrough
Step 1: Ask whether the value appears in JSX
If the value is shown, styled, or used to decide what to render, it must be state (or derived from state or props). If it is only used inside handlers or effects, a ref is enough. A stopwatch’s elapsed time is shown, so it is state; the interval id is not, so it is a ref.
Step 2: Store handles and ids in refs
function Stopwatch() {
const [startedAt, setStartedAt] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function start() {
setStartedAt(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => setNow(Date.now()), 10);
}
function stop() {
clearInterval(intervalRef.current);
}
const seconds = startedAt && now ? (now - startedAt) / 1000 : 0;
return (
<>
<p>{seconds.toFixed(2)}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}If intervalRef were state, storing the id would trigger a pointless render. If it were a local variable, stop would see undefined, because local variables reset each render.
Step 3: Use refs for DOM access in handlers and effects
function SearchBar() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Search</button>
</>
);
}React sets inputRef.current to the DOM node after commit and back to null on unmount. During the first render it is still null, which is another reason not to read it while rendering.
Step 4: Keep the latest value for long-lived callbacks
An interval or subscription created once captures one render’s values. A ref updated after each render gives it access to the latest:
const latestQuery = useRef(query);
useEffect(() => { latestQuery.current = query; });
useEffect(() => {
const id = setInterval(() => poll(latestQuery.current), 5000);
return () => clearInterval(id);
}, []);React 19.2’s useEffectEvent covers this case more cleanly when the reader is an effect.
Worked scenario
A drawing canvas tracked the pointer position in state so it could draw lines:
const [last, setLast] = useState(null);
function onPointerMove(e) {
if (last) drawLine(ctx, last, { x: e.clientX, y: e.clientY });
setLast({ x: e.clientX, y: e.clientY });
}The component re-rendered on every pointer event, over a hundred times a second, and lines appeared broken on slower laptops because each handler read the previous render’s last, which lagged behind the events.
The position is never shown in the UI, so it belongs in a ref:
const lastRef = useRef(null);
function onPointerMove(e) {
const point = { x: e.clientX, y: e.clientY };
if (lastRef.current) drawLine(ctx, lastRef.current, point);
lastRef.current = point;
}No re-renders happen while drawing, and each event reads the true previous point. A “brush size” shown in a toolbar stays in state because the UI displays it.
Common mistake
- Using a ref to “avoid re-renders” for displayed data. The screen goes stale. If the user should see it, it is state.
- Reading
ref.currentin JSX. It works by accident until rendering timing changes. The exception is lazy initialisation:if (ref.current === null) ref.current = new Heavy();. - Putting
ref.currentin a dependency array. Mutations do not trigger renders, so the effect never sees the change in time. Use a callback ref or state for that. - Thinking refs are only for DOM nodes. Any mutable value that should survive renders without causing them is a valid use.
- Copying state into a ref “to read it immediately”. If you need the next value in the same handler, compute it into a local variable, as described in state not updating immediately.
Verify the behavior
Render both counters from the How it works section next to a third button that forces a render:
export default function App() {
const [, force] = useState(0);
return (
<>
<StateCounter />
<RefCounter />
<button onClick={() => force(n => n + 1)}>re-render parent</button>
</>
);
}Click the ref counter five times: it keeps showing 0. Click “re-render parent” and it jumps to 5. Clicking the state counter updates immediately every time. That sequence shows that refs persist but do not notify React.
Follow-up questions
Why not a module-level variable instead of a ref? It would be shared across every instance of the component and survive unmounts. A ref is per instance.
What is forwardRef for, and is it still needed? It let parents pass a ref to a function component’s DOM node. In React 19, ref is an ordinary prop for function components, so forwardRef is no longer necessary (and is planned for deprecation).
What does useImperativeHandle do? It customises what a parent’s ref receives, exposing methods like focus() instead of the raw node. See useRef and imperative handles.
Does changing a ref inside an effect cause problems? No, effects and handlers are exactly where refs should be mutated.
Interview exercise
A “Send” button should be disabled while a request is in flight. A teammate writes const sending = useRef(false) and sets sending.current = true before the request and false after, with disabled={sending.current}. Users can still double-click. Explain and fix it, and say whether a ref has any role here.
Answer and reasoning
Setting sending.current does not re-render, so the button’s disabled attribute never changes; the UI shows the stale value from the last render. The visual state belongs in state: const [sending, setSending] = useState(false), with disabled={sending}. However, two clicks can land before React re-renders, so both handlers could start a request. A ref is a good guard for that: check and set inFlight.current synchronously at the start of the handler and return early if it is already true. State drives what the user sees; the ref enforces the rule between renders.
Continue learning
Continue with the React interview questions and the React MCQs. Related notes: useRef and imperative handles, state snapshots and state not updating immediately. Official reference: Referencing Values with Refs on react.dev.