useRef in React: DOM Access, Mutable Values, and Types
Use useRef for DOM nodes and mutable values that survive renders without triggering one. Covers useRef vs useState, ref callbacks, and TypeScript typing.
useRef gets introduced as "the hook for grabbing DOM elements", which is about half of what it does and the less interesting half. A ref is a box whose contents survive re-renders and whose contents changing does not cause one. That second property is the whole point, and it is what makes refs useful well beyond the DOM.
What is a ref, actually?
Calling useRef(initialValue) gives you an object with a single current property. React hands you the identical object on every render of that component. Nothing else happens. There is no subscription, no comparison, no re-render.
Everything else about refs follows from that. The useRef referenceis short for the same reason, and Manipulating the DOM with Refscovers the case that actually sends people looking, which is reaching a node React rendered.
const ref = useRef(0);
// Reading and writing are both just property access.
ref.current = ref.current + 1;
// React never notices. The screen shows whatever it showed before.
console.log(ref.current);Compare that to state, where the whole mechanism exists to tell React something changed. A ref deliberately opts out of that mechanism, which makes it the right tool for values React should not be reacting to.
Accessing DOM nodes
Pass a ref to the ref attribute of a host element and React puts the DOM node into current after it commits, then puts null back when the element is removed.
import { useRef } from 'react';
function SearchForm() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClear() {
// Imperative work React has no declarative API for.
inputRef.current?.focus();
}
return (
<form>
<input ref={inputRef} type="search" />
<button type="button" onClick={handleClear}>
Clear
</button>
</form>
);
}The legitimate list here is short: focus, text selection, scroll position, media playback, measuring layout, and handing a node to a non-React library like a charting or mapping SDK. If what you want is to change how an element looks, do it with state and let React render, not by reaching in and setting style yourself.
Why is the ref null during the first render?
This trips up everyone once. The render function runs before React creates the DOM node, so current is still null when the component body executes.
function Chart() {
const boxRef = useRef<HTMLDivElement>(null);
// TypeError: Cannot read properties of null (reading 'getBoundingClientRect')
const width = boxRef.current.getBoundingClientRect().width;
return <div ref={boxRef} />;
}Measurement belongs in an effect, after the commit. If the measurement feeds something visible, use useLayoutEffect so the browser never paints the unmeasured state, which is the difference between a correct tooltip and one that visibly jumps into position. The timing distinction is covered in more detail in useEffect in React.
useRef versus useState
The decision is one question: does the UI need to change when this value changes?
useState | useRef | |
|---|---|---|
| Triggers a re-render | Yes | No |
| Updated by | A setter function | Assigning to .current |
| Value during render | The value from this render | Whatever was last written, which may be newer |
| Safe to read while rendering | Yes | No, treat it as off limits until after commit |
| Use it for | Anything the user sees | Timer IDs, DOM nodes, bookkeeping |
A counter displayed on screen is state. A count of how many times a callback has fired, used only inside that callback, is a ref. Put the displayed counter in a ref and the number on screen simply never changes, with no error to tell you why.
Refs as instance variables
This is the use I reach for most, and it is nothing to do with the DOM. Any value that needs to persist across renders without participating in rendering goes in a ref.
Holding a timer or subscription handle
function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const intervalRef = useRef<number | null>(null);
function start() {
if (intervalRef.current !== null) return; // already running
intervalRef.current = window.setInterval(() => {
setElapsed((e) => e + 1);
}, 1000);
}
function stop() {
if (intervalRef.current === null) return;
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => stop, []); // clean up on unmount
return <output>{elapsed}s</output>;
}The interval ID has to survive re-renders so stop can clear it, and nothing on screen depends on its value. That is exactly the shape a ref fits.
Remembering the previous value of a prop
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
// Runs after render, so during render the ref still holds the old value.
ref.current = value;
}, [value]);
return ref.current;
}The ordering is the trick. The effect writes after the render has already read, so the component sees the previous value throughout its render pass.
Typing refs in TypeScript
Two patterns cover nearly everything, and the difference between them is the initial value.
// DOM refs: initialise with null, because React fills it in after commit.
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // optional chaining is required
// Value refs: initialise with the value, and current is never null.
const renderCount = useRef(0);
renderCount.current += 1; // no narrowing neededGetting the element type right matters more than it looks, because it is what gives you autocomplete on current. A few that come up constantly: HTMLInputElement, HTMLTextAreaElement, HTMLDivElement, HTMLButtonElement, HTMLCanvasElement, HTMLVideoElement. If you are unsure, hover the JSX element in your editor and the type is in the tooltip.
For a ref you assign later but cannot initialise, include undefined in the type and pass it explicitly:
// React 19's types require an argument. useRef<Connection>() is an error:
// Expected 1 arguments, but got 0.
const connectionRef = useRef<Connection | undefined>(undefined);That argument requirement is new in the React 19 type definitions and it broke a lot of existing code on upgrade. The same release also collapsed MutableRefObject into RefObject, so older tutorials telling you to pick between the two are describing a distinction that no longer exists. If the generics here are unfamiliar territory, they are the same ones covered in React and TypeScript.
Callback refs
Instead of a ref object, the ref attribute accepts a function. React calls it with the node when it mounts and with null when it unmounts. This is what you need when the set of elements is dynamic, or when you want to run code the moment a node appears.
function MessageList({ messages }: { messages: Message[] }) {
const nodes = useRef(new Map<string, HTMLLIElement>());
return (
<ul>
{messages.map((m) => (
<li
key={m.id}
ref={(node) => {
if (node) nodes.current.set(m.id, node);
// React 19: return a cleanup function instead of checking for null.
return () => nodes.current.delete(m.id);
}}
>
{m.text}
</li>
))}
</ul>
);
}The returned cleanup function is a React 19 addition. Before it, callback refs were called with null on unmount and you branched on that, which was easy to get wrong in a list. Note that an inline arrow like this one is a new function each render, so React detaches and reattaches every ref on every render. For a small list that is fine. For a large one, hoist the callback with useCallback.
Passing a ref to your own component
Refs are not props in the ordinary sense, so a parent cannot reach into a child component without the child cooperating. In React 19 the cooperation is trivial: ref arrived as a regular prop for function components.
// React 19 and later. No forwardRef.
type InputProps = {
label: string;
ref?: React.Ref<HTMLInputElement>;
};
function TextField({ label, ref }: InputProps) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// The parent focuses the inner input.
const ref = useRef<HTMLInputElement>(null);
<TextField label="Email" ref={ref} />;On React 18 and earlier this needs forwardRef, which wraps the component and gives the ref as a second argument. It still works in 19 and is deprecated rather than removed, so there is no urgency to rewrite. Write new components with the prop.
The latest ref pattern
Here is the case where refs solve a problem nothing else solves cleanly. You have a long-lived callback, a socket handler or an interval, that needs to see the current value of a prop. Listing the prop as a dependency tears down and rebuilds the connection every time it changes, which is exactly what you do not want.
function LiveFeed({ onMessage }: { onMessage: (m: Message) => void }) {
// A box that always holds the newest onMessage.
const latest = useRef(onMessage);
useEffect(() => {
latest.current = onMessage;
});
useEffect(() => {
const socket = connect();
// Reads through the ref, so it never captures a stale onMessage,
// and the socket is not torn down when the parent re-renders.
socket.on('message', (m) => latest.current(m));
return () => socket.close();
}, []); // genuinely empty: nothing here changes
}The first effect deliberately has no dependency array, so it runs after every render and keeps the box fresh. The second connects once. Without this, a parent that recreates onMessage each render would reopen the socket on every keystroke somewhere else in the tree.
React has an official answer coming for this in useEffectEvent, which does the same thing with less ceremony. It has been in the experimental channel for a while and is not in a stable release as of React 19, so this pattern is what I still write in production code. It is the one place where a ref genuinely beats every alternative rather than merely being shorter.
When is a ref the wrong answer?
Refs bypass React, and bypassing React is occasionally correct and frequently a shortcut around a structural problem.
The pattern I push back on most in review is the uncontrolled input read through a ref on submit, used to "avoid re-renders on every keystroke". It works, and for a large form it is a real optimisation that libraries like React Hook Form build on deliberately. On a three-field form it buys nothing measurable and costs you live validation, formatting as you type, and a disabled submit button. Reach for it when you have measured a problem, not by default.
The other one is mutating the DOM directly. Setting ref.current.style.display = 'none' puts the DOM out of sync with React's idea of it, and the next render will either overwrite your change or leave the element in a state neither side expects. Conditional rendering and a className handle this, declaratively, and survive the next render.
The short version
Reach for useRef when a value needs to outlive a render but must not cause one: a DOM node you have to call a method on, a timer handle, a flag that tracks whether an effect has already run, a previous-value snapshot. Reach for useState the moment the answer to "should the screen change?" is yes.
Where that leaves you is a small number of refs doing unglamorous bookkeeping at the edges of your components, which is roughly where they belong. If a component has four or five refs in it, something has usually gone wrong upstream, and the fix is more likely to be in React state management than in the refs themselves.
Frequently asked questions
What is the difference between useRef and useState?
Changing state triggers a re-render, changing a ref does not. State is updated through a setter and is safe to read while rendering. A ref is updated by assigning to its current property and should only be read in event handlers and effects, because reading or writing one during render makes the component impure and the result unpredictable once React can interrupt rendering. Use state for anything the user sees, and a ref for bookkeeping such as timer IDs, DOM nodes, and the previous value of a prop. The question that settles it every time is whether the screen should change when the value does. If yes, it is state. If nothing on screen depends on it, a ref avoids a render nobody needed. A ref that the UI does read is a bug waiting to happen, because nothing will re-render when its value changes.
Why is my ref null?
React attaches the DOM node after it commits, which is after your component function has already run. Reading ref.current in the component body during the first render gives you null, every time. Move the access into an effect, or into useLayoutEffect if you are measuring something and want to avoid a visible jump between paint and correction. The other common cause is a ref attached to a conditionally rendered element: when the condition is false there is no node, and current is null again until it renders. If you need to run code the moment a node appears or disappears, a callback ref is the right tool, because React calls it with the node on attach and with null on detach. TypeScript will usually flag this if the ref is typed with null in its initial value, which is a good reason to type it that way rather than asserting the null away.
How do I type useRef in TypeScript?
For DOM refs, write useRef with the element type and an initial value of null, then use optional chaining when reading current, because the type includes null until React attaches the node. For value refs, pass the real initial value and current is never null, so no chaining is needed. React 19 changed the types here: useRef now requires an argument, so calling it with none is a compile error, and MutableRefObject has been collapsed into RefObject. Code written against React 18 types often fails to compile after the upgrade for exactly this reason, and the fix is almost always to pass an explicit initial value rather than to widen the type. When a ref is passed to a child that may or may not attach it, keep the null in the type rather than asserting it away, because the assertion moves the failure from the compiler to runtime.
Do I still need forwardRef in React 19?
No. React 19 made ref a regular prop for function components, so you can declare ref in your props type and pass it through directly, which removes a wrapper that existed purely to work around the old rule. forwardRef still works and is deprecated rather than removed, so existing code does not need rewriting on any schedule, but new components should use the prop. The practical benefit beyond less ceremony is that the props type is now the whole contract: ref shows up in it like any other prop, rather than being attached by a higher-order function that also obscured the display name in React DevTools. Class components are the exception and still use the older mechanism, since a ref on a class already means the instance. Libraries that support both React 18 and 19 also tend to keep forwardRef for a while, so seeing it in node_modules is not a sign of stale code.


