React Hooks & Rendering
React Hooks & Rendering

useRef: mutable values & DOM refs

Full · ~25 min
After this unit you can
  • Explain that useRef returns the same mutable box every render and writing .current never re-renders
  • Choose a ref or state for a value by asking whether a change to it should update the screen
  • Spot a ref read or written during render, and move it into an event handler or effect
  • Predict that a DOM ref is null during the first render and is set only after React commits
  • Write a DOM ref that moves focus after an action, where autoFocus (focus on mount) can't reach

Read

Simple
1 / 2

A ref is a box whose .current survives re-renders

Flag

useRef(initial) gives you a plain object { current: initial }. React keeps the same object across every render, so ref.current is a stable place to stash a value — and changing it does not trigger a re-render.

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

  function handleClick() {
    clicks.current += 1; // ✅ written in an event handler: no re-render
    sendAnalytics("like", clicks.current);
  }

  return <button onClick={handleClick}>Like</button>;
}

Contrast with a normal let clicks = 0 inside the component: that resets to 0 on every render. ref.current persists. Contrast with useState: state also persists, but writing to it schedules a render. A ref writes silently.

Reach for a ref when you need to remember something between renders but the UI doesn't need to react to it — a timer id, a previous value, a "did this already run" flag.

Medium

Ref vs state — does this change need to re-render?

Flag

State and refs both persist across renders. The one question that decides between them: when this value changes, should the screen update?

  • Yes → useState. The value is shown, or drives what's shown. Writing it re-renders.
  • No → useRef. The value is bookkeeping the render doesn't read. Writing it is silent.
function Stopwatch() {
  const [elapsed, setElapsed] = useState(0); // shown → state
  const intervalId = useRef(null);           // plumbing → ref

  function start() {
    clearInterval(intervalId.current); // a second Start must not leave the first one running
    intervalId.current = setInterval(() => setElapsed((e) => e + 1), 1000);
  }
  function stop() {
    clearInterval(intervalId.current); // read it back later, no render needed
  }

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

The elapsed time is on screen, so it's state. The interval id is only there so stop() can clear it — nothing renders it — so it's a ref. Putting the id in state would cause a pointless re-render; putting elapsed in a ref would freeze the display, since mutating .current never tells React to repaint.

Complex

Reading the latest value from a long-lived callback

Flag

A callback created in one render closes over that render's variables. If it runs much later — inside a long-lived interval, a subscription, an event listener you attach once — it sees the old values. Two bad options come first to mind: add onMessage to the effect's deps and reconnect the socket every time the parent passes a new function, or leave it out and call a stale onMessage forever.

Since React 19.2, the tool for this is useEffectEvent. An Effect Event always sees the latest props and state, and it is not a dependency:

function ChatRoom({ roomId, onMessage }) {
  const onReceive = useEffectEvent((m) => onMessage(m)); // always the newest onMessage

  useEffect(() => {
    const socket = connect(roomId);
    socket.on("message", (m) => onReceive(m));
    return () => socket.close();
  }, [roomId]); // ✅ reconnects for a new room, not for a new onMessage
}

Effect Events have rules: call them only from inside effects (including callbacks the effect sets up, like this listener), never during render, and never pass them to other components or hooks. Don't use one to drop a dependency the effect really should react to: roomId stays in the array.

Quiz

What does useRef(0) return, and what happens when you write to it?

Flag

You need a value that persists across renders. When should it be a ref instead of state?

Flag

You attach a ref to an <input> to call .focus(). Where is it safe to read inputRef.current?

Flag

Which of these focus jobs needs a DOM ref, because autoFocus can't do it?

Flag

A component shows how often it has rendered with renders.current += 1 in its body. In development under StrictMode (React 19.2, which this course runs), the very first mount already shows 2. Why?

Flag

Fill in the blank

Clicking Reply should put the cursor in the comment box: declare a ref, attach it, then focus it from the click handler.

Flag
const boxRef = (null);
function handleReply() {
  boxRef..focus();
}
return (
  <>
    <button onClick={handleReply}>Reply</button>
    <textarea ={boxRef} />
  </>
);

Store an interval id without causing a re-render, then read it back to clear it.

Flag
const id = useRef(null);
id. = setInterval(tick, 1000);
// later:
clearInterval(id.);

Predict the output

function Clicks() {
  const count = useRef(0);
  return (
    <button onClick={() => { count.current += 1; }}>
      Clicked {count.current} times
    </button>
  );
}
Flag
function Field() {
  const inputRef = useRef(null);
  console.log(inputRef.current); // on the FIRST render
  return <input ref={inputRef} />;
}
Flag

Challenge

Put focus back in the search box after Clear

Flag

SearchForm has a search box and a Clear button. Clear empties the box, but focus stays on the button: the user has to click back into the input before typing the next search.

React's one declarative focus prop, autoFocus, focuses an element when it mounts. This input is already mounted when Clear is clicked, so moving focus in response to an action means calling .focus() on the real DOM node — and reaching a component's own DOM node is what a ref is for.

Your task

Edit SearchForm.tsx so the tests pass:

  • The input renders with its Search… placeholder and keeps what the user types
  • Clear empties the input
  • Clear moves focus back into the input, even though clicking the button had focused the button — and even when the box was already empty
  • With two SearchForms on the page, each Clear focuses its own form's input

Loading the in-browser editor…

Further reading