Skip to content
elephantoo

useRef: refs & the DOM

Lesson 12 of 30 14 min read

Focus inputs, measure elements, store mutable values without re-rendering, and ref as a prop in React 19.


Sometimes a component needs to remember something without re-rendering, or to reach directly into the DOM — focus an input, scroll to an element, measure its size, play a video. That's what refs are for. useRef gives you a mutable box that persists across renders and that React doesn't watch.

useRef basics#

JSX
import { useRef } from "react";

function ClickTracker() {
  const clicks = useRef(0); // { current: 0 }

  function handleClick() {
    clicks.current += 1; // mutate freely – no re-render
    console.log(`Clicked ${clicks.current} times`);
  }

  return <button onClick={handleClick}>Click me</button>;
}

useRef(initialValue) returns an object { current: initialValue }. React gives you the same object on every render, so whatever you store in current survives re-renders — but changing it doesn't trigger one.

Refs vs state

useStateuseRef
Survives re-renders✅✅
Changing it re-renders✅❌
Read during render✅ (that's the point)⚠️ Avoid
How to changesetValue(x)ref.current = x
Use forWhat's shown on screenTimer ids, DOM nodes, previous values, flags

If the value affects what the user sees, use state. If it's bookkeeping that the JSX doesn't depend on, a ref is fine.

Accessing DOM elements#

Pass a ref object to an element's ref attribute, and React puts the DOM node in ref.current:

JSX
import { useRef } from "react";

export default function SearchBox() {
  const inputRef = useRef(null);

  function focusSearch() {
    inputRef.current.focus();
    inputRef.current.select();
  }

  return (
    <>
      <input ref={inputRef} type="search" placeholder="Search lessons" />
      <button onClick={focusSearch}>Focus search</button>
    </>
  );
}

The node is assigned after React creates it, so inputRef.current is null during the first render. Read it in event handlers or effects, not in the render body.

Focusing on mount

JSX
import { useEffect, useRef } from "react";

function LoginForm() {
  const emailRef = useRef(null);

  useEffect(() => {
    emailRef.current?.focus();
  }, []);

  return <input ref={emailRef} type="email" placeholder="Email" />;
}

(For simple cases the autoFocus attribute does the same; refs give you control over when to focus.)

Scrolling an element into view

JSX
import { useRef } from "react";

export default function Gallery() {
  const listRef = useRef(null);
  const cats = Array.from({ length: 10 }, (_, i) => `Cat #${i + 1}`);

  function scrollTo(index) {
    const item = listRef.current.children[index];
    item.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollTo(0)}>First</button>
        <button onClick={() => scrollTo(9)}>Last</button>
      </nav>
      <ul ref={listRef} style={{ display: "flex", overflowX: "auto", gap: 16 }}>
        {cats.map((c) => (
          <li key={c} style={{ flex: "0 0 200px" }}>
            {c}
          </li>
        ))}
      </ul>
    </>
  );
}

Measuring elements

JSX
import { useLayoutEffect, useRef, useState } from "react";

function MeasuredBox({ children }) {
  const boxRef = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    setHeight(boxRef.current.getBoundingClientRect().height);
  }, [children]);

  return (
    <div>
      <div ref={boxRef}>{children}</div>
      <small>This box is {Math.round(height)}px tall</small>
    </div>
  );
}

useLayoutEffect measures before the browser paints, so there's no visible flicker. For sizes that change over time, use a ResizeObserver inside an effect.

Controlling media and third-party widgets

JSX
import { useRef, useState } from "react";

export default function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [playing, setPlaying] = useState(false);

  function toggle() {
    if (playing) videoRef.current.pause();
    else videoRef.current.play();
    setPlaying(!playing);
  }

  return (
    <>
      <video ref={videoRef} src={src} width={320} onEnded={() => setPlaying(false)} />
      <button onClick={toggle}>{playing ? "Pause" : "Play"}</button>
    </>
  );
}

Refs are also how you hand a DOM node to a non-React library (a chart, a map) inside an effect.

Storing values that aren't rendered#

Timer ids

JSX
import { useRef, useState } from "react";

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function start() {
    setStartTime(Date.now());
    setNow(Date.now());
    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => setNow(Date.now()), 10);
  }

  function stop() {
    clearInterval(intervalRef.current);
  }

  const seconds = startTime != null && now != null ? (now - startTime) / 1000 : 0;

  return (
    <>
      <h2>{seconds.toFixed(2)} s</h2>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

The interval id is needed by stop, but it's never displayed — perfect for a ref. (Displayed values, startTime and now, are state.)

Previous values and "latest" values

JSX
import { useEffect, useRef } from "react";

function useLatest(value) {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value; // always holds the newest value
  });
  return ref;
}

A "latest ref" lets a long-lived callback (an interval or subscription) read the newest props without re-subscribing.

Refs to your own components (React 19)#

In React 19, function components receive ref as a regular prop, so passing a ref through your own component is simple:

JSX
import { useRef } from "react";

function TextInput({ label, ref, ...props }) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  );
}

export default function Form() {
  const nameRef = useRef(null);
  return (
    <>
      <TextInput label="Name" ref={nameRef} />
      <button onClick={() => nameRef.current.focus()}>Edit name</button>
    </>
  );
}

In older code you'll see forwardRef((props, ref) => ...), which was required before React 19 and is now unnecessary for new components.

Ref callbacks

Instead of a ref object, ref can take a function. React calls it with the node when it's attached; in React 19 it can return a cleanup function, called when the node is removed:

JSX
function AutoMeasure() {
  return (
    <div
      ref={(node) => {
        const observer = new ResizeObserver(([entry]) => console.log(entry.contentRect.width));
        observer.observe(node);
        return () => observer.disconnect(); // cleanup (React 19)
      }}
    >
      Resize the window
    </div>
  );
}

Ref callbacks are handy for lists, where you can't call useRef per item.

Exposing a custom handle

useImperativeHandle lets a component expose specific methods instead of its whole DOM node:

JSX
import { useImperativeHandle, useRef } from "react";

function FancyInput({ ref }) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => {
      inputRef.current.value = "";
    },
  }));
  return <input ref={inputRef} />;
}

Use this sparingly — props and state are usually the better API.

Rules for refs#

  • Don't read or write ref.current during rendering (except to lazily initialise it). It makes rendering unpredictable. Do it in handlers and effects.
  • Prefer declarative props over imperative DOM calls. Use refs for things React can't express: focus, scroll, measurement, media playback, third-party DOM libraries.
  • Don't manually change DOM that React manages (e.g. removing children React rendered) — React will get confused. Modifying things React doesn't control, like focus or scroll position, is fine.

Common mistakes#

  • Using a ref for something displayed on screen, then wondering why the UI doesn't update.
  • Reading ref.current during the first render and getting null.
  • Calling useRef inside a loop or condition (it's a hook — top level only). Use a ref callback or a Map in one ref for lists.
  • Using document.querySelector inside components instead of refs — it can find the wrong element if a component is rendered more than once.

What's next#

Next, share values across many components without passing props through every level: context.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What happens when you change ref.current?

  2. 2.When is inputRef.current set to the DOM node for <input ref={inputRef} />?

  3. 3.In React 19, how can your own TextInput component accept a ref and pass it to its inner <input>?

Finished reading?

Mark this lesson complete to track your progress.