React refs are a way to access DOM nodes or React components directly. forwardRef is a higher-order component that allows you to pass a ref down to a child component that might not be able to receive it directly. useImperativeHandle is a hook that lets you customize the instance value that’s exposed to parent components when using refs.
Let’s see this in action. Imagine a FancyButton component that wraps a native <button> element. We want to be able to focus this button from its parent.
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
const FancyButton = forwardRef((props, ref) => {
// `useImperativeHandle` only works when using `forwardRef`
useImperativeHandle(ref, () => ({
// expose a custom method named `focus`
focus() {
// refer to the child's DOM node
node.current.focus();
}
}));
const node = useRef();
return (
<button {...props} ref={node}>
{props.children}
</button>
);
});
function App() {
const fancyButtonRef = useRef();
return (
<div>
<FancyButton ref={fancyButtonRef} onClick={() => console.log('Clicked!')}>
Click Me
</FancyButton>
<button onClick={() => fancyButtonRef.current.focus()}>
Focus the Fancy Button
</button>
</div>
);
}
export default App;
In this example, FancyButton is a functional component that receives a ref as its second argument, thanks to forwardRef. Inside FancyButton, useImperativeHandle is used to define what the parent component can access via fancyButtonRef.current. We’re exposing a focus method that, when called, will actually call the native focus() method on the underlying <button> DOM node.
The most surprising thing about refs, especially when combined with useImperativeHandle, is that they allow you to break the typical one-way data flow of React. While React components generally communicate through props and state, refs provide a direct escape hatch to imperative programming. This is powerful for tasks like managing focus, triggering animations, or integrating with third-party DOM libraries, but it should be used judiciously to avoid making your application harder to reason about.
The problem this solves is that functional components, by default, don’t accept refs. If you try to pass a ref directly to a functional component like <MyComponent ref={myRef} />, React will warn you. forwardRef is the mechanism React provides to bridge this gap, allowing you to create components that can receive and forward refs. useImperativeHandle then builds on this by letting you control what is exposed through that ref, rather than just giving access to the underlying DOM element.
Internally, forwardRef is a higher-order component. It takes a render function as an argument and returns a new component. This new component receives props and ref as arguments. The ref argument is the ref that was passed down from the parent. Your render function then needs to attach this ref to a DOM element or another component that can accept a ref.
useImperativeHandle is a hook that’s called inside a component that’s using forwardRef. It takes three arguments: the ref you want to customize, a function that returns the object you want to expose, and an optional dependency array (similar to useEffect). The object returned by the function becomes the current property of the ref passed to your component.
The exact levers you control are the methods and properties you expose via the object returned by the function passed to useImperativeHandle. You are essentially defining a custom "imperative API" for your component. This API can include methods like focus(), scrollIntoView(), or even custom logic that manipulates the component’s internal state or DOM in a way that isn’t easily achievable through props.
A common pitfall is forgetting that useImperativeHandle is only useful when combined with forwardRef. If you try to use useImperativeHandle in a regular functional component that doesn’t accept a ref via forwardRef, it won’t have any effect on the ref passed from the parent. The hook is specifically designed to customize the ref handle that is exposed through forwardRef.
The next concept you’ll likely encounter is how to manage complex state within a component that also needs to expose imperative methods, and how to ensure your imperative API remains stable and doesn’t cause unnecessary re-renders in the parent component.