Skip to content
Topic 7 of 13
Theory

useRef: DOM elements and memory without re-rendering

Why useRef is needed

State has two properties: it persists between renders, and changing it causes a re-render. Sometimes you need only the first:

  • remember a timer id so you can stop it later;
  • remember the previous value of a prop;
  • hold a reference to a DOM element to put the cursor in it, scroll to it or find out its size.

None of these values is shown on the screen, so there's no need to re-render when they change. That's what useRef is for.

How a ref works

const { useRef } = React;

function Example() {
  const clicks = useRef(0);

  function handleClick() {
    clicks.current = clicks.current + 1;
    console.log("Clicks: " + clicks.current);
  }

  return <button onClick={handleClick}>Click</button>;
}
  • useRef(initial) returns an object { current: initial }.
  • It's the same object in all renders of the component — React keeps it, like state.
  • ref.current can be read and changed directly. React doesn't know about it and doesn't re-render anything.

In the example the console shows 1, 2, 3…, while the button on the screen never changes.

Ref vs state: the difference

useState useRef
Persists between renders yes yes
Changing it causes a render yes no
How to change it setValue(new) ref.current = new
The new value is visible in the next render immediately
What for everything shown on screen service data and DOM elements

Why not just use an ordinary variable let timer? Because the component function is called anew on every render, and all its local variables are created anew. A variable with a value from the previous render is lost. A ref is a "box" that React keeps for you.

A reference to a DOM element

The most common use is access to a real page element. Pass the ref to the ref attribute, and after rendering React puts the DOM node itself into ref.current:

function SearchBar() {
  const inputRef = useRef(null);

  return (
    <div>
      <input ref={inputRef} placeholder="Search" />
      <button onClick={() => inputRef.current.focus()}>Find</button>
    </div>
  );
}

What you can do with the element:

Task Code
Put the cursor in ref.current.focus()
Select the text ref.current.select()
Scroll to the element ref.current.scrollIntoView({ behavior: "smooth" })
Find out the size ref.current.getBoundingClientRect()
Play a video ref.current.play()

During rendering, ref.current for the DOM is still null — the element doesn't exist yet. Access it in event handlers and in effects:

function AutoFocus() {
  const ref = useRef(null);
  useEffect(() => {
    ref.current.focus();      // the element is already on the page
  }, []);
  return <input ref={ref} />;
}

Don't use a ref to change what React manages (text, classes, children): React will overwrite your changes on the next render. A DOM ref is an "emergency exit" for what can't be expressed in JSX: focus, scrolling, sizes, third-party libraries.

A timer in a ref

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current !== null) return;          // already running
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  }

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

  useEffect(() => stop, []);   // stop when the component is removed

  return (
    <div>
      <p>{seconds} s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

seconds is in state: the number is shown on the screen. The interval id is in a ref: nobody sees it, but the "Stop" button needs it.

The previous value

A ref updated in an effect "lags" by one render — that's a handy way to remember the previous value:

function Score({ value }) {
  const prev = useRef(value);
  useEffect(() => {
    prev.current = value;          // after showing, remember the current one
  });
  return <p>Was {prev.current}, now {value}</p>;
}

During rendering prev.current still holds the value from last time, and after showing, the effect writes the new one there.

A ref to your own component: forwardRef

The ref attribute works on HTML tags. To pass a ref into your own component and on to an element inside it, in React 18 the component is wrapped in forwardRef:

const { forwardRef } = React;

const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

function Form() {
  const ref = useRef(null);
  return (
    <>
      <FancyInput ref={ref} placeholder="Name" />
      <button onClick={() => ref.current.focus()}>Focus</button>
    </>
  );
}

(In React 19 ref can be received as an ordinary prop, without forwardRef.)

Common mistakes

  1. Reading ref.current during rendering to show it on screen. Changing a ref won't re-render the component — the old value stays on screen. Everything the user sees belongs in state.
  2. Accessing a DOM ref before it's shown. On the first render it's null. Use an effect or a handler.
  3. let instead of a ref. An ordinary variable resets on every render.
  4. Changing the DOM through a ref instead of state. React will overwrite ref.current.textContent = ….

Summary

  • useRef(x) → { current: x }, one object for the whole lifetime of the component.
  • Changing ref.current does not cause a render. Visible things go in state, service data in a ref.
  • <input ref={ref} /> — after showing, ref.current holds the DOM element: focus(), scrollIntoView(), sizes.
  • Timer ids, previous values, "already loaded" flags — also in a ref.
  • To pass a ref into your own component — forwardRef.

Focus on a button click

Easy+10 XP

Write a component FocusForm: an <input> field and an Edit button. Clicking the button puts the cursor in the field (calls focus() on the field).

[          ] [Edit]   → click on Edit → the cursor blinks in the field

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.