useRef: DOM elements and memory without re-rendering
Why useRef is needed
State has two properties: it persists between renders, and changing it causes a re-render. Sometimes you need only the first:
- remember a timer id so you can stop it later;
- remember the previous value of a prop;
- hold a reference to a DOM element to put the cursor in it, scroll to it or find out its size.
None of these values is shown on the screen, so there's no need to re-render when they change. That's what useRef is for.
How a ref works
const { useRef } = React;
function Example() {
const clicks = useRef(0);
function handleClick() {
clicks.current = clicks.current + 1;
console.log("Clicks: " + clicks.current);
}
return <button onClick={handleClick}>Click</button>;
}
useRef(initial)returns an object{ current: initial }.- It's the same object in all renders of the component — React keeps it, like state.
ref.currentcan be read and changed directly. React doesn't know about it and doesn't re-render anything.
In the example the console shows 1, 2, 3…, while the button on the screen never changes.
Ref vs state: the difference
useState |
useRef |
|
|---|---|---|
| Persists between renders | yes | yes |
| Changing it causes a render | yes | no |
| How to change it | setValue(new) |
ref.current = new |
| The new value is visible | in the next render | immediately |
| What for | everything shown on screen | service data and DOM elements |
Why not just use an ordinary variable let timer? Because the component function is called anew on every render,
and all its local variables are created anew. A variable with a value from the previous render is lost. A ref is a
"box" that React keeps for you.
A reference to a DOM element
The most common use is access to a real page element. Pass the ref to the ref attribute, and after rendering React
puts the DOM node itself into ref.current:
function SearchBar() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} placeholder="Search" />
<button onClick={() => inputRef.current.focus()}>Find</button>
</div>
);
}
What you can do with the element:
| Task | Code |
|---|---|
| Put the cursor in | ref.current.focus() |
| Select the text | ref.current.select() |
| Scroll to the element | ref.current.scrollIntoView({ behavior: "smooth" }) |
| Find out the size | ref.current.getBoundingClientRect() |
| Play a video | ref.current.play() |
During rendering, ref.current for the DOM is still null — the element doesn't exist yet. Access it in event
handlers and in effects:
function AutoFocus() {
const ref = useRef(null);
useEffect(() => {
ref.current.focus(); // the element is already on the page
}, []);
return <input ref={ref} />;
}
Don't use a ref to change what React manages (text, classes, children): React will overwrite your changes on the next render. A DOM ref is an "emergency exit" for what can't be expressed in JSX: focus, scrolling, sizes, third-party libraries.
A timer in a ref
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return; // already running
intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => stop, []); // stop when the component is removed
return (
<div>
<p>{seconds} s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
seconds is in state: the number is shown on the screen. The interval id is in a ref: nobody sees it, but the "Stop"
button needs it.
The previous value
A ref updated in an effect "lags" by one render — that's a handy way to remember the previous value:
function Score({ value }) {
const prev = useRef(value);
useEffect(() => {
prev.current = value; // after showing, remember the current one
});
return <p>Was {prev.current}, now {value}</p>;
}
During rendering prev.current still holds the value from last time, and after showing, the effect writes the new
one there.
A ref to your own component: forwardRef
The ref attribute works on HTML tags. To pass a ref into your own component and on to an element inside it, in
React 18 the component is wrapped in forwardRef:
const { forwardRef } = React;
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
function Form() {
const ref = useRef(null);
return (
<>
<FancyInput ref={ref} placeholder="Name" />
<button onClick={() => ref.current.focus()}>Focus</button>
</>
);
}
(In React 19 ref can be received as an ordinary prop, without forwardRef.)
Common mistakes
- Reading
ref.currentduring rendering to show it on screen. Changing a ref won't re-render the component — the old value stays on screen. Everything the user sees belongs in state. - Accessing a DOM ref before it's shown. On the first render it's
null. Use an effect or a handler. letinstead of a ref. An ordinary variable resets on every render.- Changing the DOM through a ref instead of state. React will overwrite
ref.current.textContent = ….
Summary
useRef(x)→{ current: x }, one object for the whole lifetime of the component.- Changing
ref.currentdoes not cause a render. Visible things go in state, service data in a ref. <input ref={ref} />— after showing,ref.currentholds the DOM element:focus(),scrollIntoView(), sizes.- Timer ids, previous values, "already loaded" flags — also in a ref.
- To pass a ref into your own component —
forwardRef.