Typing useRef
useRef has two distinct jobs, and TypeScript types them differently depending on how you initialize it. Pointed at a DOM node, it produces a read-only ref whose .current React assigns for you. Used as a mutable box for a value that survives re-renders without triggering them, it produces a writable ref you assign yourself. Choosing the right initial value — usually null for DOM refs — is what makes the difference, and getting it wrong leads to either spurious errors or unsafe writes. This page covers DOM refs, mutable value refs, the RefObject vs MutableRefObject distinction, and forwarding refs.
DOM element refs
To reference a rendered element, initialize with null and pass the element type as the generic. React sets .current after the element mounts, so the type is T | null.
import { useRef, useEffect } from "react";
function SearchField() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // current: HTMLInputElement | null
}, []);
return <input ref={inputRef} />;
}
The generic must match the element you attach the ref to — HTMLInputElement for <input>, HTMLDivElement for <div>, and so on. Because .current is HTMLInputElement | null, the optional chain ?. (or a null check) is required before you touch it.
useRef<HTMLInputElement>(null)returns a read-onlyRefObjectwhose.currentyou should not reassign — React owns it. This is the correct overload for refs you hand to JSX via therefattribute.
Mutable value refs
To hold a value that persists across renders without causing re-renders — a timer id, a previous value, a counter — initialize with the value itself (not null). This produces a MutableRefObject<T> you are free to write to.
import { useRef, useEffect } from "react";
function Timer() {
const intervalId = useRef<number | null>(null);
const renders = useRef(0); // MutableRefObject<number>
renders.current += 1; // ✅ writable
useEffect(() => {
intervalId.current = window.setInterval(() => console.log("tick"), 1000);
return () => {
if (intervalId.current !== null) window.clearInterval(intervalId.current);
};
}, []);
return <p>Render #{renders.current}</p>;
}
renders.current += 1 is allowed because the value initializer selects the mutable overload. Mutating a ref never schedules a render, which is exactly why it suits values you want to remember but not display reactively.
The two overloads side by side
The initial argument decides which useRef overload you get, and therefore whether .current is writable.
| Call | Returned type | .current | Use for |
|---|---|---|---|
useRef<T>(null) | RefObject<T | null> | React-managed, read-only | DOM elements via ref= |
useRef<T>(initial) | MutableRefObject<T> | you assign it | mutable instance values |
useRef<T | null>(null) | MutableRefObject<T | null> | you assign it | lazily-set mutable values |
The subtle middle/last rows: passing null with a non-null generic (useRef<HTMLInputElement>(null)) selects the read-only overload, while useRef<number | null>(null) — where null is part of the generic — selects the mutable one. Match the call to the job.
Why not initialize a DOM ref undefined
A common mistake is useRef<HTMLInputElement>() with no argument. That selects the mutable overload with .current typed HTMLInputElement | undefined, but React still resets it to null on unmount, so the type lies.
// ❌ Misleading: .current is typed HTMLInputElement | undefined, but React uses null
const bad = useRef<HTMLInputElement>();
// ✅ Honest: .current is HTMLInputElement | null, matching React's behavior
const good = useRef<HTMLInputElement>(null);
Always initialize DOM refs with null so the type reflects what React actually stores. The null branch is real and you must handle it.
Forwarding refs to your own components
ref is not a normal prop, so to expose a child’s DOM node you accept a typed Ref<T>. In React 19 a ref can be passed as a regular prop; before that, use forwardRef, whose two generics are the element type and the props type.
import { forwardRef } from "react";
interface InputProps {
label: string;
}
const LabeledInput = forwardRef<HTMLInputElement, InputProps>(
({ label }, ref) => (
<label>
{label}
<input ref={ref} />
</label>
),
);
// Callers can now do <LabeledInput ref={myRef} label="Name" />
The first generic, HTMLInputElement, types the ref parameter and constrains what callers may attach; the second types the component’s props. The forwarded ref is then valid on the inner <input>.
Best Practices
- Initialize DOM refs with
nulland pass the element type:useRef<HTMLInputElement>(null). - Guard
.currentwith?.or a null check before using a DOM ref. - Use a value initializer (e.g.
useRef(0)) for mutable, non-rendering instance data. - Remember a value-initialized ref is
MutableRefObject<T>and may be reassigned freely. - Never call
useRef<T>()with no argument for a DOM ref — it produces a misleadingundefined. - Use
forwardRef<Element, Props>(or arefprop in React 19) to expose a child’s node. - Reach for state, not a ref, when a change should re-render the UI.