useRef
Stores a mutable value that survives re-renders without triggering one when it changes.
const ref = useRef(initialValue)- Added in React 16.8.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$initialValue | any | The value the ref object's current property starts out as. It can be anything: null, a DOM node placeholder, a timer id, a counter, a class instance. This argument is evaluated on every render but only used on the first one — it is ignored after the initial render. |
Return value
{ current: any }An object with a single `current` property, initially set to `initialValue`. You can mutate `current` freely; React does not re-render when you do. The same object is returned on every subsequent render. If you pass the object to a JSX node's `ref` attribute, React sets `current` to the DOM node (or component handle) while mounted and back to `null` on unmount.
Errors
useRef never throws on its own; the common runtime error is a TypeError from reading a property of ref.current while it is still null, before React has attached the DOM node or after the node has unmounted.
Changelog
- PHP 19.0.0Function components accept `ref` as a regular prop, so forwardRef is no longer needed to pass a ref down to a child component.
- PHP 19.0.0A ref callback may return a cleanup function, which React calls instead of invoking the callback again with null on detach.
- PHP 18.0.0Strict Mode in development double-invokes the component body and remounts effects, so a ref object may be created twice (one copy is discarded) and a ref attached in an effect is detached and reattached.
useRef gives you a box that React hands back unchanged on every render. Writing to ref.current does not schedule a render, and reading it does not subscribe you to anything. That makes it the right tool for values that matter to your logic but not to your output.
When to reach for it
Two distinct jobs share one Hook. First, holding a DOM node: pass the ref object as a JSX ref attribute and React fills in current, so you can call focus(), scrollIntoView(), or measure the element. Second, holding a mutable instance value — an interval id, a WebSocket, an AbortController, the previous value of a prop, a flag saying "we already ran the intro animation". If the value should appear on screen when it changes, it belongs in useState instead.
What trips people up
- Do not read or write
ref.currentduring rendering. Only initialization is safe. Rendering must be pure; mutating a ref mid-render makes output depend on how many times React chose to render. - The UI will not update. Incrementing a counter ref and expecting the number on screen to change is the single most common bug here.
currentisnullon the first render. The DOM node is attached after the commit, so touch it in an effect or an event handler, never in the body.- Refs to custom components need cooperation. A function component must accept
refas a prop and forward it to a DOM element.
Example
When to use it
- Focus a search input as soon as a dialog opens, or after a failed form submission.
- Keep the id returned by setInterval so a later handler or cleanup can clear it.
- Remember the previous value of a prop to detect the direction of a change.
- Track whether an expensive one-time setup has already run, without re-rendering.
Frequently asked questions
Why doesn't my component update when I change ref.current?
Because refs are deliberately outside React's rendering system — mutating current is a plain JavaScript assignment and React has no way to observe it. If the value is shown on screen or affects what is rendered, move it to useState. Refs are for values your logic reads (a timer id, a DOM node, a flag) that the rendered output does not depend on.
When should I use useRef instead of useState?
Use useRef when changing the value must not re-render, and useState when it must. A good test: if you deleted the value, would the rendered markup still be correct? If yes, it is a ref. Refs also keep their value synchronously — ref.current is updated the instant you assign it, whereas a state variable holds its old value for the rest of the current render.
Why is ref.current null when I try to read it?
React attaches DOM nodes after the render is committed, so current is still the initial value during the first render pass. Read it inside an event handler, a useEffect, or a useLayoutEffect instead of in the component body. It also returns to null when the element unmounts or is conditionally removed, so guard with if (ref.current) in code that can run at either time.
More examples
Holding a timer id
The tick count is state because it is displayed; the interval id is a ref because nothing on screen depends on it. Output starts at 'Ticks: 0' and increments once per second after Start. Storing the id in a plain variable instead would lose it on the next render, and Stop would have nothing to clear. Note that the guard in start() reads intervalRef.current inside an event handler, not during rendering.
import { useRef, useState } from "react";
export default function Stopwatch() {
const [ticks, setTicks] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return; // already running
intervalRef.current = setInterval(() => {
setTicks(t => t + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return (
<div>
<p>Ticks: {ticks}</p>
<button onClick={start}>Start</button>{" "}
<button onClick={stop}>Stop</button>
</div>
);
}Remembering the previous value
The ref is written in an effect, which runs after the render is committed, so the value read during the next render is the one from the previous commit. First paint shows 'Now: 0 — Before: n/a'; after one click it shows 'Now: 1 — Before: 0'. Writing ref.current directly in the component body would produce the current value instead and would violate render purity.
import { useEffect, useRef, useState } from "react";
function usePrevious(value) {
const ref = useRef(undefined);
useEffect(() => {
// Runs after commit, so during render ref.current is still the old value.
ref.current = value;
}, [value]);
return ref.current;
}
export default function Counter() {
const [count, setCount] = useState(0);
const previous = usePrevious(count);
return (
<div>
<p>
Now: {count} — Before: {previous === undefined ? "n/a" : previous}
</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}Passing a ref to your own component (React 19)
LabelledInput destructures ref like any other prop and hands it to the underlying input, so emailRef.current in the parent is the real DOM input. Submitting with an empty field moves focus back into it. In React 18 and earlier the same component had to be wrapped in forwardRef, which received ref as a second argument instead.
import { useRef } from "react";
// React 19: ref arrives as an ordinary prop — no forwardRef wrapper.
function LabelledInput({ label, ref }) {
return (
<label>
{label} <input ref={ref} />
</label>
);
}
export default function Form() {
const emailRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
if (emailRef.current.value === "") {
emailRef.current.focus();
}
}
return (
<form onSubmit={handleSubmit}>
<LabelledInput label="Email" ref={emailRef} />
<button type="submit">Submit</button>
</form>
);
}
Discussion