React ref Callback Use Cases

A ref callback is a little known feature in React, it’s a function to perform an action when React attaches or detaches a reference to a DOM element. It has a few use cases, let’s look at some.

Published · Last updated

Ref has two, related meanings in React, confusingly so. So before we start, let’s get this out of the way:

  1. The “ref object” returned by the useRef hook: a plain JavaScript object with a single property named current, which you can read or set to any value .
  2. Host elements — JSX representing DOM elements — have a special “ref attribute” that can be used to access its corresponding DOM element .

These two are very often used together, a “ref object” can be passed to a “ref attribute” and React will set a reference to the DOM element to its current property.

Besides a ref object, the ref attribute also accepts a function; a ref callback. This function receives a reference to the rendered DOM element as its only argument.

Like effect functions, React calls ref callbacks at a specific moment in the component’s cycle. A ref callback is called right after the attached DOM element is created, and again with null when the element is removed .

If a ref callback is defined as an inline function, React will call it twice every render, first with null, then with the DOM element . See the ref Callback Caveat below for more on this.

ref Callback Caveat

An inline ref callback being called twice may seem surprising, I think this behavior makes sense from Reacts perspective, it ensures consistency. A new instance of the function is created with each render. It may be a completely different function or the function may close over props or state which may have changed in the meantime.

So React needs to clear the old ref callback (call it with null) and then set up the new one (call it with the DOM element) . This allows you to attach refs conditionally and even swap them between React elements .

This may lead to it being called unnecessarily. Most of the time this is not a big deal, in some cases, this might not be what you want.

You can avoid this behavior by wrapping the ref callback in useCallback or moving the function out of the component to module scope.

Here’s a little interactive timeline that summarizes it.

As a ref callback is called after rendering, it is safe to perform side-effects in it .

The React Docs don’t have that much to say about ref callbacks . Perhaps they intentionally don’t discuss it much because use cases are fairly rare and accessing DOM elements is an escape hatch .

A ref callback is definitely a niche feature of React (strictly speaking of react-dom) and you won’t need it every day. Still, it has some legitimate use cases, it wouldn’t be in React if it didn’t! So let’s look at some use cases.

First, I want to put this front and center:

Ref callbacks are only necessary when you need the underlying DOM element.

This may sound obvious but it’s good to clear any confusion before moving forward. Just because you can perform side-effects in them doesn’t mean you should. If you need to perform non-DOM side-effects, that’s what useEffect is for.

So when are ref callbacks useful?

Use a ref callback when you want to perform an action when React attaches or detaches a ref to a DOM element.

As far as I know, that boils down to these four situations.

  1. Call a DOM elements instance method to perform a side-effect when an element mounts or updates.
  2. Be ‘notified’ of the changes to a DOM element; re-render when some property of a DOM element has changed.
  3. Put a reference to a DOM element in state to read it during rendering.
  4. Share the DOM ref; do multiple things with the DOM element reference.

Now, let us look at use cases for each.

You can call DOM element instance methods on mount with a ref callback to perform DOM side-effects like scrolling or focusing.

For example, scroll automatically to the last item in a long list when it’s added:


function scrollTo(el) {
// On first render and on unmount there
// is no DOM element so `el` will be `null`
// so we need to check if it exists
if (el) {
el.scrollIntoView({ behavior: "smooth" });
}
}
function List({ data }) {
return (
<ul>
{data.map((d, i) => {
const isLast = i === data.length - 1;
// ref callback to scroll to the last list element
return (
<li
key={d.name}
ref={isLast ? scrollTo : undefined}
>
{d.name}
</li>
);
})}
</ul>
);
}

Remember, managing the DOM is Reacts job, avoid all DOM-mutating methods, like insert*(), remove*(), set*(), replace*(), append*() .

Also note that the browser doesn’t allow certain DOM element methods to be called without user interaction, like requestFullscreen. All such protected methods won’t do anything when called in a ref callback.

dialogEl.showModal() in ref callback

I thought a ref callback could be a way to open <dialog> with showModal(). Because:

It is recommended to use the .show() or .showModal() methods to render dialogs, rather than the open attribute. If a <dialog> is opened using the open attribute, it will be non-modal.

I assumed a ref callback could help to open it automatically:

<dialog ref={el => el?.showModal()}>

But a quick try showed that it doesn’t work and will raise this error:

Failed to execute 'showModal' on 'HTMLDialogElement': The element already has an 'open' attribute, and therefore cannot be opened modally.

There was already an issue for this in the React repo, and apparently it’s a feature, not a bug. It has to do with behavior in Strict Mode for the upcoming offscreen API.

In Strict Mode, React simulates remounting a component with existing state. This is to prepare for the feature that would allow to persist state between remounts: In this case, running showModal() twice does not work. So Strict Mode is uncovering a problem with this code.


Usually, the solution is to make the effect symmetric and add cleanup.


useEffect(() => {
const dialog = ref.current;
dialog.showModal();
return () => dialog.close();
}, []);

You can use a ref callback when you want React to be in the know of some DOM element property. Read a DOM element property like scroll position or call a method that provides information on the element like getBoundingClientRect() in a ref callback and set that info to state.

This is a snippet straight from the old React docs: How can I measure a DOM node?. It’s a great example for this ref callback use case so I’ll just copy it here.


const [size, setSize] = useState();
const measureRef = useCallback((node) => {
setSize(node.getBoundingClientRect());
}, []);
return <div ref={measureRef}>{children}</div>;

We didn’t choose useRef in this example because an object ref doesn’t notify us about changes to the current ref value. Using a ref callback ensures that even if a child component displays the measured node later (e.g. in response to a click), we still get notified about it in the parent component and can update the measurements

Similarly, we may want to use the length of an SVG <path /> element to set its strokeDasharray.


const [pathLength, setPathLength] = useState(0);
return (
<path
strokeDasharray={pathLength / 2} // show just half of the path
ref={(el) => el && setPathLength(el.getTotalLength())}
d="..."
/>)

If a state-setter function is passed to a ref attribute, the reference to the DOM element will be set to state. It will trigger a new render because that’s just what setting state does. It won’t fall into an infinite render loop because the state-setter is a stable function , therefore the ref callback is called only on mount and on unmount.

Why would we prefer this over useRef? Because reading a ref object in render is not allowed . If we need the DOM element in render it must be in state.

Don’t read or write ref.current during rendering. If some information is needed during rendering, use state instead. Since React doesn’t know when ref.current changes, even reading it while rendering makes your component’s behavior difficult to predict.

A Portal beams a component to a different location in the DOM, most often to show an overlay, like a modal or sidebar, from deep in the component tree to ensure it’s displayed on top. It comes in useful when the shape of the component tree doesn’t match the shape of the DOM tree, more on that soon.


// Assume an empty div with id 'modal' is in your HTML
const modalEl = document.getElementById("modal");
function Modal({ children, ...props }) {
return ReactDOM.createPortal(
<ModalBase {...props}>
{children}
</ModalBase>,
modalEl
);
}

Obtaining a reference to the DOM element with document.getElementById() is OK if you can be sure it exists. But maybe you don’t have control over the HTML. Maybe you want to portal to a DOM element created by React.

To do so, we need to read a reference to the DOM element in render. That’s where a ref callback comes in.


function Parent() {
const [modalElement, setModalElement] = useState(null);
return (
<div>
<div id="modal-location" ref={setModalElement} />
{/* Imagine that the modal container and the
Modal itself are farther apart in the component tree */}
<Modal modalElement={modalElement}>Warning</Modal>
</div>
)
}
function Modal({ children, modalElement, ...props }) {
return modalElement
? ReactDOM.createPortal(
<ModalBase {...props}>{children}</ModalBase>,
modalElement
)
: null;
}

The modalElement is null initially so it needs to be checked before creating the portal.

Uncontrolled Compound Components is an advanced React pattern with a ref callback at its heart for a React-managed portal.

Compound Components

Compound component are components that combine and work together to form one useful piece of UI. Compound component splitss complex functionality into smaller chunks that work together. This is preferable to having a single ‘God-component’ with a ton of props. Remember, React composes, make use of it!

You’re already familiar with this idea if you know these HTML element combinations! .

  • <option> in a <select> element
  • <table> with <thead>, <tbody> and <tr> and cell elements.
  • A <summary> in a <details> element
Try to find all compound HTML elements in my visualization of HTML elements!

Read more on compound components:

I’ll give a quick summary here but I’ll mostly refer you to these posts by Jenna Smith who coined the term Uncontrolled Compound Components . Take your time to read, it took me a couple of times to fully grasp it.

Smarter, Dumb Breadcrumb Diagram & Summary

I made this diagram with tldraw for myself to fully understand how the Smarter, Dumb Breadcrumbs work. Maybe it helps you.

Components as nested boxes
Summary

Each <Breadcrumb/> will portal to to breadcrumbElement DOM element in <BreadcrumbPortal/>.

<BreadcrumbPortal/> will display the <Breadcrumbs /> in the order they were rendered.

If <BreadcrumbPortal/> is not rendered, breadcrumbElement will be null and <Breadcrumb/> will render nothing.

In React, data always flows down. When the data flow doesn’t fit the structure of the component tree, we can adapt the data flow by lifting state up. Most of the time this is a good solution.

For shared components — like dialogs or sidebars of which there can be only one on the page — lifting state can make it ‘too global’. That state will be wired through many intermediate components that don’t really need to know about it. It taints that chain of components and can make your code a veritable tangle.

[…] Components are for re-use. Why do we move our content away from the place that controls it and introduce a bunch of global state to re-use something?

We’re shoehorning the component tree and data flow to follow the shape of the DOM tree, that’s the root of the problem. Instead, we can invert control by adapting the component tree to the data flow.

What if we applied DRY by bundling re-used pieces of our UI into components that can be consumed in multiple places of the app, instead of controlling a single instance of a component from multiple places.

Using a Portal, we can reuse the location in the DOM for that shared component and keep the DOM structure the same. It enables keeping related components close, even if they aren’t close in the DOM. This makes our React code easier to follow.

Why was I trying to re-use a single [component] instance when a) I only want to reuse its content that is relevant [here], meaning b) it is really just the location of it that I want to re-use.

The ingredients of Uncontrolled Compound Components are.

  1. A ref callback to grab the location. Get a reference to the DOM element and put it in state. We can’t use a ref object because we need it in render and schedule an update when it’s set.
  2. A Context to share the location. Put the reference to the DOM element in a context so all components within the context have access to this location in the DOM.
  3. A Portal to jump to the location. Grab the reference to the DOM element (the location!) from the context and portal the component into it .

If you’re thinking “Boy all of this sounds complex”, well, you’re not wrong. It’s an advanced pattern and does take some set-up but it offers writing simpler components in return. That makes it worth it. You probably won’t find yourself needing it very often but when you do it is sweet.

‘Uncontrolled Compound Components’ In the WildLibraries
  • The component primitives library RadixUI uses ‘Uncontrolled Compound Components’ to great effect. Check out the example code for a dialog, the button that triggers it is next to the dialog component itself, wow such colocation! It may seem more tedious than using a single configurable component but composing components like this makes it much more declarative, flexible, and thus more React-y too. Also, it’s no coincidence that that same Jenna used to be on the RadixUI team.
  • CMD+K; Fast, unstyled command menu React component. follows the same approach as RadixUI. The architecture page in the repo has a nice explanation of why they went for compound components.

[W]e wanted to render components. Especially, we wanted full component composition. Compound components are natural and easy to write. A few months after exploring this library, we were pleased to see Radix UI released using this exact approach of component structure – setting the standard for ease of use and composability.

Times It Came In Useful For Me

I’ve used it a few times and it melted away a lot of hard-to-follow, sprawly code.

  • The textbook case: The configuration sidebar on my side-project TernaryPlot.com, it’s a sidebar that contains configurations for a plot element which is far removed from the sidebar in the DOM tree.
  • In SVG, the z-index CSS property doesn’t do anything, this tripped me up countless times. In SVG, the render order is based purely on the DOM order. This is where a Portal can help. In my HTML elements mental map, I used Uncontrolled Compound Components to render the element attribute circles on top.

Sometimes more than one consumer needs access to a DOM element. Say you want to measure the width of a <div> and hand it off to another library that does DOM stuff outside of Reacts control, which is fine as long as the element’s content is not managed by React .

A practical example is creating a responsive chart using a library like D3 (using d3-selection) or @observable/plot.

In this example, I use @observable/plot to add a box plot. To make it responsive, I use react-use-measure to measure the DOM element. I use a ref callback to pass the DOM element to both:


import useMeasure from "react-use-measure";
import * as Plot from "@observablehq/plot";
export function BoxPlot({ data }) {
const [measureRef, { width, height }] = useMeasure({ debounce: 5 });
const plotRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const boxPlot = Plot.plot({
width: Math.max(150, width),
marks: [
Plot.boxX(data),
],
});
plotRef.current.append(boxPlot);
return () => boxPlot.remove();
}, [data, width]);
const initBoxPlot = useCallback((el: HTMLDivElement | null) => {
plotRef.current = el;
measureRef(el);
}, []);
return <div ref={initBoxPlot} />;
}

See the ref callback caveat for why the ref callback wrapped in useCallback.

  • A ref callback is a function passed to a host element’s ref attribute. React will call it with a reference to the DOM element when it mounts or and with null when it unmounts.
  • React will also call your ref callback whenever you pass a different ref callback.
  • ref callbacks let you perform an action when React attaches or detaches a ref to a DOM element.
  • You can use it to
    • Perfom DOM side-effects like scrolling or focussing on mount.
    • Make React aware of DOM properties like dimensions or scroll position.
    • Use Portals with React-controlled DOM elements.
      • “Uncontrolled Compound Components” can be powerful pattern for colocating components, it makes use of React-managed Portals.
    • Pass the DOM element to multiple consumers.

Have you learned something from this post? Was something unclear? Have I missed a good ref callback use case? Am I wrong about something? Just let me know on Twitter!

  1. You can point a ref to any value. However, the most common use case for a ref is to access a DOM element. For example, this is handy if you want to focus an input programmatically. When you pass a ref to a ref attribute in JSX, like <div ref={myRef}>, React will put the corresponding DOM element into myRef.current. […] A ref is a plain JavaScript object with a single property called current, which you can read or set.
    React Docs: Referencing Values with Refs - Refs and the DOM
    2
  2. React also supports another way to set refs called “ref callbacks”, which gives more fine-grain control over when refs are set and unset. Instead of passing a ref attribute created by [useRef], you pass a function. The function receives the React [class] component instance or HTML DOM element as its argument, which can be stored and accessed elsewhere.
    React docs: Refs and the DOM — ref callbacks
    2
  3. Another solution is to pass a function to the ref attribute. This is called a “ref callback”. React will call your ref callback with the DOM node when it’s time to set the ref, and with null when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID.
    React Docs: Manipulating the DOM with Refs - How to manage a list of refs using a ref callback
    2
  4. Instead of a ref object (like the one returned by useRef), you may pass a function to the ref attribute.
    When the <div> DOM node is added to the screen, React will call your ref callback with the DOM node as the argument. When that <div> DOM node is removed, React will call your ref callback with null.
    React will also call your ref callback whenever you pass a different ref callback. In the above example, (node) => { ... } is a different function on every render. When your component re-renders, the previous function will be called with null as the argument, and the next function will be called with the DOM node.
    React DOM Docs: Common components – ref callback function
    2
  5. If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the ref callback as a bound method on the class, but note that it shouldn’t matter in most cases.
    React docs: Refs and the DOM — Caveats with ref callbacks
  6. ref is set to the DOM node when it’s mounted, and set to null when the DOM node is removed. No surprises this far. One thing to note here is that a ref is, strictly speaking, never updated. If a DOM node is replaced by some other node (say, its DOM tag or key changes), the ref is unset, and then set to a new node. […] The part I was not aware of is that the identity of ref prop also forces it to update. When a ref prop is added, it’s set to DOM node. When a ref prop is removed, the old ref is set to null. Here, again, the ref is unset, [then] set again. This means that if you pass an inline [function] as a ref, it’ll go through unset / set cycle on every render. So, why does it work that way? In short, it allows you to attach refs conditionally and even swap them between components […]. There’s a subtle difference between having an [inline functions/callbacks] as your ref prop and a ref object or a stable callback — the [inline function/callback] has a new identity on every render, forcing the ref to go through an update cycle [where it will be] null. This is normally not too bad, but good to know.
    Thoughtspile.tech: So you think you know everything about React refs
  7. A cool and useful implication of this is that you can safely put expensive side effects in a ref callback, as long as ref identity does not change. For example, a typical DOM measuring logic:
    Thoughtspile: So you think you know everything about React refs — You can side effect in ref callback
  8. One rudimentary way to measure the position or size of a DOM node is to use a ref callback. React will call that callback whenever the ref gets attached to a different node. […] We didn’t choose useRef in this example because an object ref doesn’t notify us about changes to the current ref value. Using a ref callback ensures that even if a child component displays the measured node later (e.g. in response to a click), we still get notified about it in the parent component and can update the measurements. Note that we pass [] as a dependency array to useCallback. This ensures that our ref callback doesn’t change between the re-renders, and so React won’t call it unnecessarily. In this example, the ref callback will be called only when the component mounts and unmounts, since the rendered <h1> [element] stays present throughout any rerenders.
    React docs: Hooks FAQ — How can I measure a DOM node?
  9. […] React does not let a component access the DOM nodes of other components. Not even for its own children! This is intentional. Refs are an escape hatch that should be used sparingly. Manually manipulating another component’s DOM nodes makes your code even more fragile.
  10. Refs are an escape hatch. You should only use them when you have to “step outside React”. Common examples of this include managing focus, scroll position, or calling browser APIs that React does not expose.
    If you stick to non-destructive actions like focusing and scrolling, you shouldn’t encounter any problems. However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making.
    Manipulating the DOM with Refs - Best practices for DOM manipulation with refs
  11. React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency array.
    React Docs Hooks Reference: useState
  12. You shouldn’t read (or write) the current value during rendering. / You can read state at any time. However, each render has its own snapshot of state which does not change.
    Referencing Values with Refs — Differences between refs and state
  13. I’ve begun referring to this colocation technique in React as the Uncontrolled Compound Component pattern. These are components that do everything associated with their concern without you needing to wire them up manually to use them. No state, no refs. Everything Just Works™.
    We’ve taken this approach on the Radix Primitives team at Modulz to encapsulate the accessibility and functionality requirements of common web components so you can get up and running quickly.
    Avoid Global State — Co-locate with Uncontrolled Compound Components
  14. Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Features like context work exactly the same regardless of whether the child is a portal, as the portal still exists in the React tree regardless of position in the DOM tree. This includes event bubbling. An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.
    […]
    Catching an event bubbling up from a portal in a parent component allows the development of more flexible abstractions that are not inherently reliant on portals. For example, if you render a Modal component, the parent can capture its events regardless of whether it’s implemented using portals.
    React Docs: Portals – Event Bubbling Through Portals
  15. Avoid changing DOM elements managed by React. Modifying, adding children to, or removing children from elements that are managed by React can lead to inconsistent visual results or crashes [...].

    However, this doesn’t mean that you can’t do it at all. It requires caution. You can safely modify parts of the DOM that React has no reason to update. For example, if some <div> is always empty in the JSX, React won’t have a reason to touch its children list. Therefore, it is safe to manually add or remove elements there.