How to Type useRef in React with TypeScript

Type the useRef Hook correctly in TypeScript: useRef now requires an argument and returns one mutable RefObject type for both DOM nodes and plain values.

6 min read

In React 19, useRef always requires an argument and always returns a single mutable RefObject, whether the ref holds a DOM node or a plain value. Choosing the right type argument still matters for autocomplete and for catching mistakes before they reach the browser, but the old split between a read-only DOM ref and a separate mutable value ref is gone.

This changed in React 19

Before React 19, calling useRef with an element type and null returned a read-only RefObject, and a separate MutableRefObject type covered writable refs. React 19 unified both into one mutable RefObject, and MutableRefObject is deprecated. Projects still on React 18 types will see the older read-only behavior.

A DOM ref

For a ref that points at a DOM node, pass the element type and null.

App.tsxApp.tsx
import { useRef } from "react";
 
function Search() {
  const inputRef = useRef<HTMLInputElement>(null);
 
  return <input ref={inputRef} />;
}

The ref's current is typed HTMLInputElement or null, and TypeScript now allows writing to it. React still owns the DOM assignment in practice, so treat current as something you read, not something you write, even though the compiler no longer stops you. The element type parameter tells TypeScript which DOM methods, like focus, are available once current is not null.

A mutable value ref

When the ref holds a value rather than a node, pass the value's type and an initial value.

App.tsxApp.tsx
import { useRef } from "react";
 
function Counter() {
  const countRef = useRef<number>(0);
 
  return (
    <button onClick={() => (countRef.current += 1)}>
      Clicked {countRef.current} times
    </button>
  );
}

current is a number you can read and write. The button updates the ref silently, but the label never changes because a ref does not trigger a re-render. Values used for display belong in state instead. Value refs are the right type for timer IDs and other values that never appear in the JSX.

A ref that starts empty, like a timer ID

Some refs must start empty but hold a value later, like a timer ID that does not exist until the timer starts.

App.tsxApp.tsx
import { useRef } from "react";
 
function Chat() {
  const timeoutRef = useRef<number | null>(null);
  function handleSend() {
    timeoutRef.current = window.setTimeout(() => {}, 2000);
  }
  return <button onClick={handleSend}>Send</button>;
}

Passing a union like number | null gives current the type number or null, and current is mutable in React 19. You can assign the timer ID once it exists and read it back to clear the timer. This pattern suits timer IDs, observers, and other values that start unset.

Calling useRef with no value

React 19 requires an argument, so useRef alone no longer compiles. Pass undefined explicitly when a ref has no meaningful starting value yet.

App.tsxApp.tsx
import { useRef, useEffect } from "react";
 
function Widget() {
  const cleanupRef = useRef<(() => void) | undefined>(undefined);
 
  useEffect(() => {
    cleanupRef.current = () => console.log("cleaned up");
    return () => cleanupRef.current?.();
  }, []);
 
  return <p>Widget</p>;
}

This gives current the type of a cleanup function or undefined, and TypeScript accepts the call because an argument was supplied.

The overloads at a glance

Every call below returns a mutable RefObject in React 19. The type argument only changes what current can hold, not whether it is writable.

CallType of current
Element type with nullHTMLInputElement or null
Concrete valuenumber
Element or value type with undefinednumber or undefined

What to learn next

Typing refs is one part of typing components. Continue with how useRef works for the runtime behavior, and how to type React props with TypeScript for the wider picture.

Rune AI

Rune AI

Key Insights

  • React 19 requires useRef to receive an argument; call useRef(undefined) instead of useRef().
  • useRef now returns one mutable RefObject type, so MutableRefObject is deprecated.
  • Passing an element type with null still marks the ref for a DOM node.
  • Passing a concrete value gives a mutable current of that type.
  • Changing a ref's current never triggers a re-render, no matter how it is typed.
RunePowered by Rune AI

Frequently Asked Questions

Is a DOM ref's current still read-only in TypeScript?

Not in React 19. useRef now always returns a single mutable RefObject, so current is writable at the type level even for DOM refs. Older React 18 types returned a separate read-only RefObject for the null-initialized case.

Do I still need a union type like number or null for a nullable mutable ref?

No. In React 19, useRef<T>(null) already returns a mutable RefObject<T | null>, so the union workaround is only needed on React 18 types.

What does useRef() with no argument return?

It is a type error in React 19. useRef now requires an argument, so call useRef(undefined) to get a mutable ref typed as T or undefined.

Conclusion

In React 19, useRef always requires an argument and always returns a single mutable RefObject, whether it holds a DOM node or a plain value. Pass an element type with null for a DOM ref, or a concrete value for a value ref.