How to expose a custom ref handle in React

René Kulik on 06.12.2022

In this blogpost I am going to show you how you can expose a custom ref handle in React using the useImperativeHandle hook. This is extremely handy when you want to use compontent internal functionality outside its function scope.

Customize the exposed instance value

In general, components to not expose their DOM nodes to their parent. To access those nodes, you have to use forwardRef:

const Child = forwardRef((props, ref) => {
  const handleClick = () => console.log('click!');

  return (
    <button ref={ref} onClick={handleClick}>
      {props.children}
    </button>
  );
});

const Parent = () => {
  const childRef = useCallback(node => {
    if (node) {
      console.log(node);
    }
  }, []);

  return (
    <Child ref={childRef}>
      I am a button
    </Child>
  );
};

In this example we have two components, Child and Parent. By using a ref, the Parent gets access to the <button> DOM node of the Child. When running the example above, you will see the following output in your console:

<button>I am a button</button>

To expose a custom handle instead of a DOM node, use useImperativeHandle:

const Child = forwardRef((props, ref) => {
  const handleClick = () => console.log('click!');

  useImperativeHandle(ref, () => ({ handleClick }));

  return (
    <button onClick={handleClick}>
      {props.children}
    </button>
  );
});

const Parent = () => {
  const childRef = useCallback(handle => {
    if (handle) {
      console.log(handle);
      handle.handleClick();
    }
  }, []);

  return (
    <Child ref={childRef}>
      I am a button
    </Child>
  );
};

Note that the ref is no longer assigned to the <button>. Instead of a node, the parent receives an object containing the handleClick function. The following will be logged to the console:

{handleClick: ƒ}
click!

Real-world example

Now as you know how to customize the exposed instance value of a component, you might ask yourself when this would be useful.

I recently implemented a notification functionality where I used this technique. For showing notifications I created a dedicated provider which stores a message in its state. Whenever there is a message, the Notification component will be rendered:

export const NotificationProvider = forwardRef((props, ref) => {
  const { children, action } = props;
  const [message, setMessage] = useState();
  const showNotification = setMessage;
  const closeNotification = () => setMessage(undefined);

  useImperativeHandle(ref, () => ({ closeNotification }));

  return (
    <NotificationContext.Provider value={{ showNotification, closeNotification }}>
      {children}
      {message && <Notification action={action}>{message}</Notification>}
    </NotificationContext.Provider>
  );
});

The provider also lets you configure an action via props. This action will be used globally for all notifications and gets rendered inside the Notification component. In my case I passed in a button with an onClick event handler. Whenever you click on the button the exposed provider function closeNotification gets executed and closes the notification:

export const App = () => {
  const notificationProviderRef = useRef(null);

  return (
    <NotificationProvider ref={notificationProviderRef} action={<button onClick={() => notificationProviderRef.current?.closeNotification()}>close</button>}>
      <Content />
    </NotificationProvider>
  );
};

That’s it. In this blogpost I described how you can expose a custom ref handle by using useImperativeHandle. This is the way to go if you want to make child functionality accessible for the parent.