Skip to content
TypeScript ts react 4 min read

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-only RefObject whose .current you should not reassign — React owns it. This is the correct overload for refs you hand to JSX via the ref attribute.

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.

CallReturned type.currentUse for
useRef<T>(null)RefObject<T | null>React-managed, read-onlyDOM elements via ref=
useRef<T>(initial)MutableRefObject<T>you assign itmutable instance values
useRef<T | null>(null)MutableRefObject<T | null>you assign itlazily-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 null and pass the element type: useRef<HTMLInputElement>(null).
  • Guard .current with ?. 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 misleading undefined.
  • Use forwardRef<Element, Props> (or a ref prop in React 19) to expose a child’s node.
  • Reach for state, not a ref, when a change should re-render the UI.
Last updated June 29, 2026
Was this helpful?