Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 10 min read

useRef

Remembering something without waking React up

Builds on: useState
In one sentence

useRef is a little box React keeps between passes. Putting something in it does not cause a redraw – which is exactly the point.

shape.ts
const box = useRef(initial);

box.current            // look inside
box.current = value;   // put something in  ->  React does NOT redraw
Put another way

useState is a scoreboard: write on it and everyone looks. useRef is a note in your pocket: it stays with you, but nobody notices when you change it.

Purpose 1: touching an element on the page

Sometimes you have to tell the browser something directly: “put the cursor here”, “scroll there”, “play this video”. For that you need the real element – and React puts it in the box for you.

dom.tsx
function Search() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </>
  );
}
The ref attribute tells React: “put this element in my box as soon as it is on the page”.
Good to know That is why it starts out null

On the first pass the element does not exist yet – React builds it afterwards. So you reach for it with ?. or from inside a click handler.

Purpose 2: remembering something quietly

remember.tsx
// The id of a timer, so you can cancel it later
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

function onType() {
  if (timerRef.current) clearTimeout(timerRef.current);
  timerRef.current = setTimeout(search, 500);
}

// How many times did it draw? (just to look at, not to display)
const counter = useRef(0);
counter.current += 1;

State or ref? One single question

Does the picture have to change?
ref – the display stays at 0
const countRef = useRef(0);

function click() {
  countRef.current += 1;
}

return <p>{countRef.current}</p>;
state – the number is visible
const [count, setCount] = useState(0);

function click() {
  setCount((c) => c + 1);
}

return <p>{count}</p>;

On the left the number in the box really does go up – but React never redraws, so you always see 0. Visible → state. Invisible → ref.

Reach for it when …
  • +

    Touching an element: focus(), scrolling, measuring, controlling a video

  • +

    Keeping timer ids around

  • +

    Remembering the previous value of something

  • +

    Holding objects from other libraries (a map, a chart, an editor)

Skip it when …
  • Values that should appear on screen – that is state

  • As a trick to “save” redraws – the display then freezes

  • Reading or writing in the middle of drawing

Careful Do not touch it while drawing

Read and write refs in click handlers or in useEffect – not directly in the function body. Otherwise the result depends on when React happens to draw, and React cannot promise you that.

In more depth: callback refs and ref as a prop optional

Instead of a box you can also hand the ref attribute a function. React calls it as soon as the element lands on the page – and again when it leaves. Handy when you want to measure something right afterwards.

callback-ref.tsx
function Box() {
  const [height, setHeight] = useState(0);

  return (
    <div
      ref={(node) => {
        if (node) setHeight(node.getBoundingClientRect().height);
        // React 19: whatever you return here serves as the cleanup
      }}
    >

    </div>
  );
}
Good to know React 19: no more forwardRef

ref is now an ordinary prop: function Input({ ref, ...rest }) { … }. Older code still uses forwardRef for this – that keeps working, it is just no longer necessary.

Does it stick?

4 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Focus a field and count quietly

Two jobs: the button should put the cursor in the input, and next to it you should count how often the component drew – without the counting itself causing a draw.

  • Clicking Focus puts the cursor in the field
  • The number next to Renders goes up as you type
  • The counting itself causes no extra draw
import { useRef, useState } from "react";

export default function App() {
  const [text, setText] = useState("");
  // TODO: ref for the input field
  // TODO: ref for the counter

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <input aria-label="field" value={text} onChange={(e) => setText(e.target.value)} />{" "}
      <button onClick={() => { /* TODO: focus */ }}>Focus</button>
      <p data-testid="renders">Renders: 0</p>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →