DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Migrating from React Router v5 to v6: A Comprehensive Guide
  • Why React Router 7 Is a Game-Changer for React Developers
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide

Trending

  • Transforming AI-Driven Data Analytics with DeepSeek: A New Era of Intelligent Insights
  • Kubeflow: Driving Scalable and Intelligent Machine Learning Systems
  • AI-Based Threat Detection in Cloud Security
  • Revolutionizing Financial Monitoring: Building a Team Dashboard With OpenObserve
  1. DZone
  2. Coding
  3. JavaScript
  4. React Callback Refs: What They Are and How to Use Them

React Callback Refs: What They Are and How to Use Them

Learn how React callback refs enhance DOM manipulation, address common issues, and enable advanced component interactions.

By 
Yusup Izripov user avatar
Yusup Izripov
·
Feb. 07, 25 · Analysis
Likes (2)
Comment
Save
Tweet
Share
4.5K Views

Join the DZone community and get the full member experience.

Join For Free

During development, we often need direct interaction with DOM elements. In such cases, React provides us with a mechanism called refs, which allows access to elements after they have been rendered. Most commonly, we use standard object refs via useRef (let’s call them that), but there is another approach known as callback refs. This method offers additional flexibility and control over the lifecycle of elements, enabling us to perform certain specific actions at precise moments when elements are attached or detached from the DOM. 

In this article, I want to explain what callback refs are and how they work, discuss the pitfalls you might encounter, and show examples of their usage.

What Are Callback Refs and How Do They Work?

Callback refs give you more granular control over ref attachment compared to object refs. Let’s take a look at how they work in practice:

  1. Mounting. When an element mounts into the DOM, React calls the ref function with the DOM element itself. This allows you to perform actions with the element immediately after it appears on the page.
  2. Unmounting. When an element unmounts, React calls the ref function with null. This gives you the opportunity to clean up or cancel any actions associated with that element.

Example: Tracking Mount and Unmount

TypeScript
 
import React, { useCallback, useState } from 'react';

function MountUnmountTracker() {
  const [isVisible, setIsVisible] = useState(false);

  const handleRef = useCallback((node: HTMLDivElement | null) => {
    if (node) {
      console.log('Element mounted:', node);
    } else {
      console.log('Element unmounted');
    }
  }, []);

  return (
    <div>
      <button onClick={() => setIsVisible((prev) => !prev)}>
        {isVisible ? 'Hide' : 'Show'} element
      </button>
      {isVisible && <div ref={handleRef}>Tracked element</div>}
    </div>
  );
}

export default MountUnmountTracker;


Each time we toggle the visibility of the element, the handleRef function is called with either the node or null, allowing us to track the moment the element is attached or detached.

Common Issues and Solutions

Issue: Repeated Callback Ref Invocations

A frequent issue when using callback refs is the repeated creation of the ref function on each re-render of the component. Because of this, React thinks it’s a new ref, calls the old one with null (cleaning it up) and then initializes the new one — even if our element or component has not actually changed. This can lead to unwanted side effects.

Example of the Problem

Consider a Basic component that has a button for toggling the visibility of a div with a callback ref, plus another button to force a component re-render:

TypeScript
 
import React, { useState, useReducer } from 'react';

function Basic() {
  const [showDiv, setShowDiv] = useState(false);
  const [, forceRerender] = useReducer((v) => v + 1, 0);

  const toggleDiv = () => setShowDiv((prev) => !prev);

  const refCallback = (node: HTMLDivElement | null) => {
    console.log('div', node);
  };

  return (
    <div>
      <button onClick={toggleDiv}>Toggle Div</button>
      <button onClick={forceRerender}>Rerender</button>
      {showDiv && <div ref={refCallback}>Example div</div>}
    </div>
  );
}

export default Basic;


Every time you click on the Rerender button, the component re-renders, creating a new refCallback function. As a result, React calls the old refCallback(null) and then the new refCallback(node), even though our element with the ref has not changed. In the console, you’ll see div null and then div [node] in turn, repeatedly. Obviously, we usually want to avoid unnecessary calls like that.

Solution: Memoizing the Callback Ref With useCallback

Avoiding this is quite straightforward: just use useCallback to memoize the function. That way, the function remains unchanged across re-renders, unless its dependencies change.

TypeScript
 
import React, { useState, useCallback, useReducer } from 'react';

function Basic() {
  const [showDiv, setShowDiv] = useState(false);
  const [, forceRerender] = useReducer((v) => v + 1, 0);

  const toggleDiv = () => setShowDiv((prev) => !prev);

  const refCallback = useCallback((node: HTMLDivElement | null) => {
    console.log('div', node);
  }, []);

  return (
    <div>
      <button onClick={toggleDiv}>Toggle Div</button>
      <button onClick={forceRerender}>Rerender</button>
      {showDiv && <div ref={refCallback}>Example div</div>}
    </div>
  );
}

export default Basic;


Now, refCallback is created only once, on the initial render. It will not trigger extra calls on subsequent re-renders, preventing unnecessary callbacks and improving performance.

The Order of Callback Refs, useLayoutEffect, and useEffect

Before we talk about how to use callback refs in your code to solve specific problems, let’s understand how callback refs interact with the useEffect and useLayoutEffect hooks so that you can properly organize resource initialization and cleanup.

Execution Order

  1. callback ref – called immediately after rendering DOM elements, before effect hooks are run
  2. useLayoutEffect – runs after all DOM mutations but before the browser paints
  3. useEffect – runs after the component has finished rendering to the screen
TypeScript
 
import React, { useEffect, useLayoutEffect, useCallback } from 'react';

function WhenCalled() {
  const refCallback = useCallback((node: HTMLDivElement | null) => {
    if (node) {
      console.log('Callback ref called for div:', node);
    } else {
      console.log('Callback ref detached div');
    }
  }, []);

  useLayoutEffect(() => {
    console.log('useLayoutEffect called');
  }, []);

  useEffect(() => {
    console.log('useEffect called');
  }, []);

  return (
    <div>
      <div ref={refCallback}>Element to watch</div>
    </div>
  );
}

export default WhenCalled;


Console Output

  1. Callback ref called for div: [div element]
  2. useLayoutEffect called
  3. useEffect called

This sequence tells us that callback refs are triggered before hooks like useLayoutEffect and useEffect, which is essential to keep in mind when writing your logic.

Which Problems Do Callback Refs Solve in Code?

First, let’s reproduce a problem typically encountered with regular object refs so we can then solve it with callback refs.

TypeScript
 
import { useCallback, useEffect, useRef, useState } from 'react';

interface ResizeObserverOptions {
  elemRef: React.RefObject<HTMLElement>;
  onResize: ResizeObserverCallback;
}

function useResizeObserver({ elemRef, onResize }: ResizeObserverOptions) {
  useEffect(() => {
    const element = elemRef.current;

    if (!element) {
      return;
    }

    const resizeObserver = new ResizeObserver(onResize);

    resizeObserver.observe(element);

    return () => {
      resizeObserver.unobserve(element);
    };
  }, [onResize, elemRef]);
}

export function UsageDom() {
  const [bool, setBool] = useState(false);
  const elemRef = useRef<HTMLDivElement>(null);

  const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
    console.log('resize', entries);
  }, []);

  useResizeObserver({ elemRef, onResize: handleResize });

  const renderTestText = () => {
    if (bool) {
      return <p ref={elemRef}>Test text</p>;
    }

    return <div ref={elemRef}>Test div</div>;
  };

  return (
    <div style={{ width: '100%', textAlign: 'center' }}>
      <button onClick={() => setBool((v) => !v)}>Toggle</button>
      {renderTestText()}
    </div>
  );
}


We won’t dive into every detail here. In short, we’re tracking the size of our div or p element via a ResizeObserver. Initially, everything works fine: on mount, we can get the element’s size, and resizes are also reported in the console.

The real trouble starts when we toggle state to switch the element we’re observing. When we change state and thus replace the tracked element, our ResizeObserver no longer works correctly. It keeps observing the first element, which is already removed from the DOM! Even toggling back to the original element doesn’t help because the subscription to the new element never properly attaches.

Note: The following solution is more representative of what you might write in a library context, needing universal code. In a real project, you might solve it through a combination of flags, effects, etc. But in library code, you don’t have knowledge of the specific component or its state. This is exactly the scenario where callback refs can help us.

TypeScript
 
import { useCallback, useRef, useState } from 'react';

function useResizeObserver(onResize: ResizeObserverCallback) {
  const roRef = useRef<ResizeObserver | null>(null);

  const attachResizeObserver = useCallback(
    (element: HTMLElement) => {
      const resizeObserver = new ResizeObserver(onResize);
      resizeObserver.observe(element);
      roRef.current = resizeObserver;
    },
    [onResize]
  );

  const detachResizeObserver = useCallback(() => {
    roRef.current?.disconnect();
  }, []);

  const refCb = useCallback(
    (element: HTMLElement | null) => {
      if (element) {
        attachResizeObserver(element);
      } else {
        detachResizeObserver();
      }
    },
    [attachResizeObserver, detachResizeObserver]
  );

  return refCb;
}

export default function App() {
  const [bool, setBool] = useState(false);

  const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
    console.log('resize', entries);
  }, []);

  const resizeRef = useResizeObserver(handleResize);

  const renderTestText = () => {
    if (bool) {
      return <p ref={resizeRef}>Test text</p>;
    }

    return <div ref={resizeRef}>Test div</div>;
  };

  return (
    <div style={{ width: '100%', textAlign: 'center' }}>
      <button onClick={() => setBool((v) => !v)}>Toggle</button>
      {renderTestText()}
    </div>
  );
}


As you can see, we rewrote our useResizeObserver hook to use a callback ref. We just pass in a (memoized) callback that should fire on resize, and no matter how many times we toggle elements, our resize callback still works. That’s because the observer attaches to new elements and detaches from old ones at the exact time we want, courtesy of the callback ref. 

The key benefit here is that the developer using our hook no longer needs to worry about the logic of adding/removing observers under the hood — we’ve encapsulated that logic within the hook. The developer just needs to pass in a callback to our hook and attach its returned ref to their elements.

Combining Multiple Refs Into One

Here’s another scenario where callback refs come to the rescue:

TypeScript
 
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';

interface InputProps {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
}

const Input = forwardRef(function Input(
  props: InputProps,
  ref: React.ForwardedRef<HTMLInputElement>
) {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!inputRef.current) {
      return;
    }

    console.log(inputRef.current.getBoundingClientRect());
  }, []);

  return <input {...props} ref={ref} />;
});

export function UsageWithoutCombine() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const focus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <Input ref={inputRef} />
      <button onClick={focus}>Focus</button>
    </div>
  );
}


In the code above, we have a simple input component on which we set a ref, grabbing it from props using forwardRef. But how do we use inputRef inside the Input component if we also need the ref from the outside for something like focusing? Maybe we want to do something else in the input component itself, such as getBoundingClientRect. Replacing the prop ref with our internal ref means focusing from the outside will no longer work. So, how do we combine these two refs?

That’s where callback refs help again:

TypeScript
 
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';

type RefItem<T> =
  | ((element: T | null) => void)
  | React.MutableRefObject<T | null>
  | null
  | undefined;

function useCombinedRef<T>(...refs: RefItem<T>[]) {
  const refCb = useCallback((element: T | null) => {
    refs.forEach((ref) => {
      if (!ref) {
        return;
      }

      if (typeof ref === 'function') {
        ref(element);
      } else {
        ref.current = element;
      }
    });
  }, refs);

  return refCb;
}

interface InputProps {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
}

const Input = forwardRef(function Input(
  props: InputProps,
  ref: React.ForwardedRef<HTMLInputElement>
) {
  const inputRef = useRef<HTMLInputElement>(null);
  const combinedInputRef = useCombinedRef(ref, inputRef);

  useEffect(() => {
    if (!inputRef.current) {
      return;
    }

    console.log(inputRef.current.getBoundingClientRect());
  }, []);

  return <input {...props} ref={combinedInputRef} />;
});

export function UsageWithCombine() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const focus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <Input ref={inputRef} />
      <button onClick={focus}>Focus</button>
    </div>
  );
}


Explanation

We implemented a useCombinedRef hook where a developer can pass in standard refs, callback refs, and optionally null or undefined. The hook itself is just a useCallback that loops over the refs array. If the argument is null, we ignore it, but if the ref is a function, we call it with the element; if it’s a standard ref object, we set ref.current to the element. 

In this way, we merge multiple refs into one. In the example above, both getBoundingClientRect inside the Input component, and the external focus call will work correctly.

What Changed in React 19 Regarding Callback Refs?

Automatic Cleanup

React now automatically handles the cleanup of callback refs when elements unmount, making resource management simpler. Here’s an example the React team shows in their documentation:

TypeScript
 
<input
  ref={(ref) => {
    // ref created

    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () => {
      // ref cleanup
    };
  }}
/>


You can read more details about it in the official blog post, where it’s mentioned that soon, cleaning up refs via null might be deprecated, leaving a single standardized way to clean up refs.

Choosing Between Normal Refs and Callback Refs

  • Use standard refs (useRef) when you just need simple access to a DOM element or want to preserve some value between renders without additional actions on attach or detach.
  • Use callback refs when you require more granular control over the element’s lifecycle, when you are writing universal code (for example, in your own library or package), or when you need to manage multiple refs together.

Conclusion

Callback refs in React are a powerful tool that gives developers extra flexibility and control when working with DOM elements. In most cases, standard object refs via useRef are sufficient for everyday tasks, but callback refs can help with more complex scenarios like those we discussed above.

Element React (JavaScript library) JavaScript

Opinions expressed by DZone contributors are their own.

Related

  • Migrating from React Router v5 to v6: A Comprehensive Guide
  • Why React Router 7 Is a Game-Changer for React Developers
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: