React
useRef
React Hooks
TypeScript
DOM
Frontend

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.

10 min read
Chamikara Nayanajith

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 a ref actually is

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.

tsx
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.

tsx
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.

The ref is 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.

tsx
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?

useStateuseRef
Triggers a re-renderYesNo
Updated byA setter functionAssigning to .current
Value during renderThe value from this renderWhatever was last written, which may be newer
Safe to read while renderingYesNo, treat it as off limits until after commit
Use it forAnything the user seesTimer 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

tsx
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

tsx
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.

tsx
// 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 needed

Getting 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:

tsx
// 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.

tsx
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.

tsx
// 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.

tsx
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 a ref is 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. Use state for anything the user sees, and a ref for bookkeeping such as timer IDs and DOM nodes.

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. Move the access into an effect, or into useLayoutEffect if you are measuring something and want to avoid a visible jump.

How do I type useRef in TypeScript?

For DOM refs, write useRef<HTMLInputElement>(null) and use optional chaining when reading current. For value refs, pass the initial value and current is never null. React 19 types require an argument, so useRef<T>() with no argument is now a compile error, and MutableRefObject has been collapsed into RefObject.

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. forwardRef still works and is deprecated rather than removed, so existing code does not need rewriting, but new components should use the prop.

Related Articles