Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A10
43 / 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

⚛A10 · Part 3 · Advanced

Refs in depth

Forwarding, imperative handles, and integrating libraries that own their own DOM.

4 min read8 sections2 exercises
43 of 57

In this chapter

  • ref is a prop again
  • useImperativeHandle: exposing an API instead of a node
  • Callback refs, and the cleanup nobody knew about
  • Merging refs
  • Integrating a library that owns its DOM

and 3 more

about 4 min
08On this page
  1. ref is a prop again
  2. useImperativeHandle: exposing an API instead of a node
  3. Callback refs, and the cleanup nobody knew about
  4. Merging refs
  5. Integrating a library that owns its DOM
  6. Focus management, which is mostly refs
  7. Fragment refs: one ref for a group of nodes
  8. The rules that keep refs safe

ref is a prop again

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

// before 19, and in every existing codebase
const TextField = forwardRef(function TextField(props, ref) {
  return <input ref={ref} {...props} />;
});

forwardRef existed because ref was intercepted by React rather than passed through. In 19 function components receive it normally. You will still read forwardRef constantly, so knowing both forms matters more than preferring one.

useImperativeHandle: exposing an API instead of a node

function Editor({ ref }) {
  const inner = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inner.current.focus(),
    clear: () => { inner.current.value = ""; },
  }), []);

  return <textarea ref={inner} />;
}

editorRef.current.focus();      // the parent gets two methods, not a DOM node

Handing out the raw node lets a parent do anything to it — including things that fight React. A narrow handle is a contract: these are the imperative operations this component supports.

Use it rarely, and for a reason

A component that needs commanding from outside is usually one that should have taken a prop. ref.current.open() and <Modal isOpen /> do the same job; the second is declarative, testable, and survives being rendered twice. Reach for an imperative handle when the action genuinely has no state — focus, scroll, play, reset — where modelling it as state means inventing a flag you then have to clear.

Callback refs, and the cleanup nobody knew about

<div ref={(node) => {
  if (!node) return;
  const observer = new ResizeObserver(onResize);
  observer.observe(node);
  return () => observer.disconnect();     // React 19: cleanup from a ref callback
}} />

Before 19, a callback ref was called with null on unmount and you cleaned up in that branch. React 19 lets it return a cleanup function directly, which makes ref callbacks a genuine alternative to an effect for anything scoped to a node's lifetime.

The callback must be stable or memoised. An inline arrow is a new function each render, so React detaches and reattaches — running your setup and teardown on every render.

Merging refs

function useMergedRefs(...refs) {
  return useCallback((node) => {
    for (const ref of refs) {
      if (typeof ref === "function") ref(node);
      else if (ref) ref.current = node;
    }
  }, refs);
}

<input ref={useMergedRefs(localRef, forwardedRef, register("email").ref)} />

An element takes one ref, and libraries want their own — a form library, a drag-and-drop hook, and your own measurement all at once. This ten-line helper is why react-merge-refs exists.

Integrating a library that owns its DOM

function Map({ center, markers }) {
  const container = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    map.current = new MapLib(container.current);      // create once
    return () => map.current.destroy();
  }, []);

  useEffect(() => { map.current?.setCenter(center); }, [center]);   // sync props in
  useEffect(() => { map.current?.setMarkers(markers); }, [markers]);

  return <div ref={container} />;      // React never touches inside this
}

The pattern is always the same: one effect to create and destroy, one per prop to synchronise, and a container React renders empty and then leaves alone. React must not manage children of a node another library controls — they will fight, and the reconciler will lose in confusing ways.

Focus management, which is mostly refs

useEffect(() => {
  const previous = document.activeElement;      // remember where focus was
  dialogRef.current?.focus();
  return () => previous?.focus();               // put it back on close
}, []);

Returning focus to the element that opened a dialog is the difference between a keyboard user continuing where they were and being dumped at the top of the document. It is three lines and it is skipped constantly.

Fragment refs: one ref for a group of nodes

Before React 19.3, a ref could only point at one element, so observing or listening to a group of siblings meant adding a wrapper <div> — which changes layout, breaks CSS grid and flex, and adds a node the accessibility tree has to explain. Since 19.3 a <Fragment> takes a ref, and you get a FragmentInstance.

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

function SeenTracker({ children, onSeen }) {
  const ref = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries.some((e) => e.isIntersecting)) onSeen();
    });
    ref.current.observeUsing(observer);          // every first-level DOM child
    return () => ref.current?.unobserveUsing(observer);
  }, [onSeen]);

  return <Fragment ref={ref}>{children}</Fragment>;   // no wrapper div
}
MethodWhat it touches
addEventListener, removeEventListener, dispatchEventFirst-level DOM children
observeUsing, unobserveUsingFirst-level DOM children, with an IntersectionObserver or ResizeObserver
getClientRectsRects of the first-level DOM children
focus, focusLast, blurThe first or last focusable node, searched deeply
scrollIntoView, compareDocumentPosition, getRootNodeThe fragment's position in the document
  • The shorthand cannot take it. <>...</> accepts neither key nor ref; write <Fragment ref={ref}>.
  • First-level means first-level. Listeners and observers attach to the fragment's direct DOM children, not to elements nested inside them. Focus is the exception and searches all the way down.
  • Hidden Activity trees are skipped. Listeners are added when an <Activity> becomes visible, not before.
  • focus() is the practical win. A modal or a list can focus its first focusable child without knowing what it is — which is the focus-management problem above, solved without a wrapper.

The rules that keep refs safe

  • Never read or write ref.current during render. Rendering must be pure and may run twice.
  • Do not fight React for a node it owns. Setting textContent on something React renders will be overwritten, or worse, will not be.
  • A ref is not state. If the UI must change when it changes, it is state.
  • Prefer declarative. An imperative handle is an escape hatch, and every one you add is a way for a parent to break your component's assumptions.
Interview answer, one sentence

"Refs are the escape hatch to the DOM and to values that persist without rendering. useImperativeHandle narrows what a parent can do to a small API instead of a raw node, and the standard pattern for a third-party widget is one effect to create and destroy plus one per prop to synchronise — with React never rendering inside the container."

02Practice this chapter

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

  • 8 testsKeep a chat log scrolled to the newest messageadvanced
  • 5 testsExpose an imperative method through a refadvanced
All 54 for this level →
A10

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A9Previous · A9useSyncExternalStore
  2. A10you are here
  3. Next · A11Advanced component patternsThe four shapes behind every component library you have imported.3 min read

01On this page

8 sections · 0%

  1. 01ref is a prop again
  2. 02useImperativeHandle: exposing an API instead of a node
  3. 03Callback refs, and the cleanup nobody knew about
  4. 04Merging refs
  5. 05Integrating a library that owns its DOM
  6. 06Focus management, which is mostly refs
  7. 07Fragment refs: one ref for a group of nodes
  8. 08The rules that keep refs safe

[] chapters · t top