In this blog post 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 blog post 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.