Skip to the chapter
←ReactGGroundwork⚛React
React/Intermediate/I1
12 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛I1 · Part 2 · Intermediate

useRef

A box that survives renders and does not cause them.

3 min read8 sections4 exercises
12 of 57

In this chapter

  • Two jobs, one hook
  • Job one: reaching a DOM node
  • Job two: a value that must survive renders
  • The classic use: previous value
  • The other classic: the latest callback

and 3 more

about 3 min
08On this page
  1. Two jobs, one hook
  2. Job one: reaching a DOM node
  3. Job two: a value that must survive renders
  4. The classic use: previous value
  5. The other classic: the latest callback
  6. Callback refs
  7. Refs to your own components
  8. When a ref is the wrong answer

Two jobs, one hook

useRef returns an object with a single current property. React keeps that object for the life of the component and never replaces it. Changing current does not trigger a render.

const ref = useRef(initial);   // { current: initial }, same object forever

That one behaviour covers two very different uses.

Job one: reaching a DOM node

function SearchBox() {
  const input = useRef(null);

  useEffect(() => {
    input.current.focus();          // after the DOM exists
  }, []);

  return <input ref={input} />;
}

React sets input.current to the DOM node after committing, and back to null on unmount. During the first render it is null — which is why DOM work goes in an effect, not in the render body.

Legitimate reasons to hold a DOM node: focus, text selection, scroll position, measuring, playing media, and handing the element to a non-React library like a chart or a map. Reading or writing anything React also controls is not on that list.

Job two: a value that must survive renders

const timer = useRef(null);

function start() {
  timer.current = setInterval(tick, 1000);   // no re-render, correctly
}
function stop() {
  clearInterval(timer.current);
}

A plain variable would be recreated each render; state would re-render the component for a value nothing displays. A ref is exactly the middle case: remembered, but not rendered.

useStateuseRef
Survives re-rendersyesyes
Changing it re-rendersyesno
Can be read during renderyesnot safely
Updated bya setter, replacingassigning to .current
The rule that keeps refs safe

Do not read or write ref.current during rendering. Rendering must be pure and repeatable; a ref is mutable state that React does not track, so reading it mid-render makes the output depend on something React cannot see. Refs belong in event handlers and effects.

The classic use: previous value

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;      // after render, so reads see the old one
  });
  return ref.current;
}

const prevCount = usePrevious(count);   // undefined on first render

The timing is the whole trick: the effect writes after the render that read it, so the ref is always one render behind.

The other classic: the latest callback

function useInterval(callback, delay) {
  const saved = useRef(callback);

  useEffect(() => { saved.current = callback; }, [callback]);   // always current

  useEffect(() => {
    const id = setInterval(() => saved.current(), delay);
    return () => clearInterval(id);
  }, [delay]);                                                  // interval not restarted
}

Without the ref, the interval closes over the first render's callback and calls a stale function forever. Adding callback to the second effect's dependencies would fix the staleness by tearing down and recreating the interval on every render, which makes the timer drift. The ref gives you a fresh function without restarting anything — this is the pattern behind most "latest ref" code you will see in libraries.

Callback refs

A ref prop can also be a function. React calls it with the node when it mounts and with null when it unmounts, which is useful when you need to run something the moment the node exists.

<input ref={(node) => { if (node) node.focus(); }} />

Define it outside render or memoise it — an inline arrow is a new function each render, so React detaches and reattaches the ref every time.

Callback refs are also the answer to a list of refs, where useRef cannot help because you do not know the count in advance:

const rows = useRef(new Map());

<li ref={(node) => {
  if (node) rows.current.set(item.id, node);
  else rows.current.delete(item.id);
}}>

Refs to your own components

In React 19, ref is an ordinary prop for function components, so a component can accept one and forward it:

function TextField({ ref, ...rest }) {
  return <input ref={ref} {...rest} />;
}

Before 19 this required forwardRef, which you will still meet in every existing codebase and most libraries:

const TextField = forwardRef(function TextField(props, ref) {
  return <input ref={ref} {...props} />;
});

useImperativeHandle narrows what the parent gets — instead of the whole DOM node, expose a small API such as { focus, clear }. Reach for it rarely; a component that needs to be commanded from outside is often one that should have taken a prop.

When a ref is the wrong answer

const count = useRef(0);
count.current++;                     // ✗ the screen never updates
return <p>{count.current}</p>;

If the value appears on screen, it is state. The question is not "does this change?" but "does the UI need to change when it changes?" — yes means state, no means ref.

Interview answer, one sentence

"A ref is a mutable box that persists across renders without causing them, which makes it right for DOM nodes and for values the UI does not display — timers, previous values, the latest callback. Reading it during render breaks purity, so it belongs in handlers and effects."

04Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsThe box behind usePreviousintermediate
  • 8 testsFocus the box, and give focus backintermediate
  • 8 testsusePreviousintermediate
  • 4 testsA ref per list item, without extra stateintermediate
All 66 for this level →
I1

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. B11Previous · B11Guided project: a task board
  2. I1you are here
  3. Next · I2Context APIA way to skip the middle layers — not a state manager, and not free.3 min read

01On this page

8 sections · 0%

  1. 01Two jobs, one hook
  2. 02Job one: reaching a DOM node
  3. 03Job two: a value that must survive renders
  4. 04The classic use: previous value
  5. 05The other classic: the latest callback
  6. 06Callback refs
  7. 07Refs to your own components
  8. 08When a ref is the wrong answer

[] chapters · t top