useRef: values that survive without rendering
A ref is a box that keeps its value between renders and never triggers one. How that differs from state and a local variable, and how refs reach DOM nodes in React 19.
After this lesson you can
- Choose between state, a ref and a local variable for a value
- Read the latest value from inside an old closure with a ref
- Attach a ref to a DOM node, including passing it through a component in React 19
useRef(initial) returns an object { current: initial }. React hands
back the same object on every render of that component, and changing
ref.current is an ordinary property write: React does not know about it
and does not re-render.
That puts a ref between the two other places a component can keep a value:
| survives a re-render | changing it re-renders | |
|---|---|---|
local variable (let x) | no — reset every render | no |
useRef | yes | no |
useState | yes | yes |
// A miniature host: the hook slots persist, the function body re-runs.function createHost(component) { const slots = { state: 0, ref: { current: 0 } }; const screen = []; let handlers; const renderOnce = () => { handlers = component(slots, () => renderOnce()); screen.push(handlers.output); }; renderOnce(); return { click: (name) => handlers[name](), screen };} function Clicker(slots, rerender) { let local = 0; // a plain variable const ref = slots.ref; // useRef(0): same object every render const count = slots.state; // useState(0) return { output: `state=${count} ref=${ref.current} local=${local}`, local: () => { local++; }, ref: () => { ref.current++; }, state: () => { slots.state = count + 1; rerender(); }, };} export function run() { const host = createHost(Clicker); host.click("ref"); host.click("ref"); host.click("local"); host.click("state"); return host.screen;}The ref counted every click, but the screen kept showing the value from the last render until something else (the state click) caused one. The local variable was back to 0 because each render runs the function body from the top.
Use a ref for values the component needs to remember but that do not affect what it renders: a timer id, the previous value of a prop, a flag for "request in flight", an instance of a third-party object. If the value appears in the JSX, it should be state.
Do not read or write refs during render
Rendering must be pure. Reading ref.current in the JSX shows whatever
the last mutation left there, which may not correspond to any render. Read
and write refs in event handlers and effects. The one accepted exception
is lazy initialisation: if (ref.current === null) ref.current = new Player().
The latest value inside an old closure
The previous lesson's stale closures have a ref-based fix: write the
newest value into a ref after every render, and have the long-lived
callback read ref.current instead of the captured variable.
export function run() { const latest = { current: null }; // useRef(null) const callbacks = []; const renders = ["draft 1", "draft 2", "draft 3"]; renders.forEach((text, i) => { latest.current = text; // updated after every render if (i === 0) { // Set up once, like an effect with [] dependencies: callbacks.push(() => `captured: ${text}`); callbacks.push(() => `via ref: ${latest.current}`); } }); // The timer fires after the third render: return callbacks.map((f) => f());}Refs to DOM nodes
Passing a ref object as the ref attribute of a DOM element asks React to
put the DOM node in ref.current when it is attached, and to set it back to
null when it is removed:
function Search() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}
The node is only there after commit, so use it in handlers and effects,
never during render. In React 19, a function component receives ref as
a normal prop and can pass it on to an inner element — forwardRef is no
longer needed for that. A ref callback (ref={node => …}) is called
with the node, and in React 19 it may return a cleanup function that runs
when the node is detached. To expose only a few methods instead of the
whole node, a component can use useImperativeHandle.
Common mistakes
- Rendering a value kept in a ref. It goes stale on screen, because changing the ref never triggers a render, as the example models.
- Using a plain
letto remember something between renders. It is reset every render. - Reading
ref.currentof a DOM ref during the first render. It is stillnull; the node is attached at commit. - expecting the effect to re-run when changes. Mutating triggers nothing.
Exercise · Pro
A stopwatch that will not stop
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade