In React, what does useRef(initialValue) return, and why does assigning to ref.current not re-render the component the way a useState setter does?
answer
- a box, not a subscription
- identity stable across renders
- nothing watches a property write
- state repaints, refs do not
- initial argument applied only once
basics
~20 suseRef returns a stable mutable object shaped { current: initialValue } that survives every render. Writing to .current mutates that object directly instead of going through React's update queue, so React is never notified and schedules no re-render.
solid answer
~50 s`useRef(initialValue)` creates one object on the first render and hands back that **same object identity** on every later render, with a single mutable `current` property. A state setter is a message to React: it enqueues an update and marks the component for re-render. `ref.current = x` is just a property write on an ordinary JavaScript object — nothing subscribes to it, so React has no idea it happened and the screen keeps showing the old value until something else triggers a render. That is why refs are the right home for values the UI does not display: a timer id, the previous value of a prop, a DOM node, a mutable third-party instance. If a change must appear on screen it belongs in state; if a change must never by itself repaint, it belongs in a ref.
code
javascript · 17 linesimport { useRef, useState } from 'react';
export function ClickCounters() {
const refCount = useRef(0);
const [stateCount, setStateCount] = useState(0);
function handleClick() {
refCount.current += 1;
setStateCount((n) => n + 1);
}
return (
<button onClick={handleClick}>
ref: {refCount.current} / state: {stateCount}
</button>
);
}go deeper
Be able to say plainly that useRef returns an object with a current property that persists across renders, and that changing current does not update the screen. Name one honest use: a timer id or a DOM node.
Explain the mechanism, not the rule: React re-renders on state updates, parent renders, context and store notifications, and a property write is none of them. Also explain that the initial argument is applied only on the first render.
Show judgment about which values deserve a ref in a real codebase — latches, timer handles, previous values, library instances — and be ready to call out the anti-pattern of demoting displayed state to a ref to shave renders.
Frame it as an API-contract question: refs are an untracked mutable escape hatch, so every ref in a codebase is a value outside React's data flow. Be ready to say where you allow them and what conventions keep them from becoming ambient shared state.
## Two places to keep a value A React function component runs top to bottom on every render, so any plain `let` declared inside it is created and discarded with that call. A component therefore needs somewhere outside the call to keep values, and React offers exactly two such places. They differ in one respect only: whether changing the value tells React to render again. `useState` gives you a snapshot plus a setter. The value you read is fixed for the duration of that render, and the setter is a message to React — enqueue an update, re-run this component, paint the result. `useRef` gives you a box. `useRef(initialValue)` returns an object shaped `{ current: initialValue }`. React creates it during the first render, stores it in that component's hook slot, and on every later render returns the *same* object — `===` to the one you got before. Nothing about the object is magic: it is an ordinary mutable JavaScript object that React keeps alive on your behalf. ## Why the write is invisible to React `ref.current = 5` is a plain property assignment. There is no setter to intercept it, no proxy, no subscription. React does not observe object mutation anywhere in its model — a component re-renders because of a state update, a parent re-rendering, a context change, or an external-store notification. A ref write is none of those. React is not slow to notice it; React never notices it. The practical consequence is a classic bug. Render `{ref.current}` in JSX, mutate `ref.current` in a click handler, and the DOM keeps showing the old number forever. Then it appears to "work" the moment something unrelated re-renders the component, which produces the puzzling report: *the counter only updates when I type in the other field*. ```jsx function BrokenCounter() { const clicks = useRef(0); // clicking mutates the box, but nothing repaints the label return <button onClick={() => { clicks.current++; }}>clicked {clicks.current}x</button>; } ``` ## The argument is used once The value you pass is applied only on the first render. On every later render React ignores it — but JavaScript still evaluates the expression before calling the hook, so `useRef(new ExpensiveThing())` constructs and throws away an object on every single render. That waste is why the lazy-initialisation idiom (`if (ref.current === null) ref.current = ...`) exists. ## What actually belongs in a ref Good candidates share one trait: changing them should not repaint anything. - the id returned by `setInterval` or `setTimeout`, needed only so cleanup can clear it - a DOM node you want to call a method on - a mutable instance owned by a third-party library - the previous value of a prop, kept for comparison - a latch such as "this form has already been submitted" The corollary matters as much: moving displayed state into a ref to "avoid re-renders" is not an optimisation, it is a bug. You removed the re-render *and* the update. If the user must see it, it is state. ## useRef versus createRef `createRef()` allocates a brand-new ref object on every call. In a class constructor, which runs once per instance, that is correct. In a function component body, which runs on every render, it silently discards the previous render's reference — you get a fresh empty box each time. `useRef` exists precisely because function bodies re-run; it is the hook-slot-backed version of the same idea. ## Identity is stable, contents are not Because React returns the same object every time, the ref object itself is a stable value. You can close over it in effects and callbacks without listing it in a dependency array, and the hooks linter knows this. What is *not* stable is `ref.current` — it is mutable by design, and React never re-runs anything when it changes. Depending on the current value is a genuinely different problem from depending on the box. ## Typing note With the React 19 type definitions, `useRef` requires an argument, so a DOM ref is written `useRef<HTMLInputElement | null>(null)` rather than `useRef<HTMLInputElement>()`. The runtime behaviour is unchanged; only the types got stricter.
- If a ref never triggers a re-render, how does a value stored in a ref ever reach the screen?Only by riding along with a render caused by something else — a state update, a parent re-render, a context change. That is exactly why refs are a poor home for displayed values: the display is then at the mercy of unrelated updates and looks stale or laggy.
- Is the argument you pass to useRef re-evaluated on later renders?React ignores it after the first render, but JavaScript still evaluates the expression on every render before the call. So `useRef(new Foo())` builds a throwaway `Foo` each time. When construction is expensive, pass `null` and create the value lazily instead.
- Why is createRef wrong inside a function component?`createRef()` returns a new object on every call, and a function component body runs on every render, so each render gets a fresh empty ref and last render's node is lost. It suits a class constructor, which runs once. `useRef` keeps one object in the component's hook slot.
saying these in an interview costs you the question
- Says writing to ref.current re-renders the component
- Stores displayed values in a ref to avoid re-renders
- Thinks useRef's argument is re-applied on every render
- Calls createRef inside a function component body
- Believes refs can only ever hold DOM nodes