Among all the native React hooks that exist, there are well known ones like useState
, useEffect
and less known ones like useImperativeHandle
.
This last hook is the subject of this article that I teased in my last article about React references
.
At the end of this post, you will know what problem is solved with it, how to use it and a real use case where it’s needed to be more concrete.
Buckle up, let’s go.
In React, like in some other libraries, the data flow is unidirectional going top-down in the component tree. It means that a parent component can configure a child component thanks to properties. So in a sense, a child can have access to an element of the parent when passed as property. But a parent can’t have access to an element of the child, or so you think.
If you want to expose a callback from the Child it can expose a register
to register this callback
:
function Child({ register }) {
useEffect(() => {
const aFunction = () =>
console.log('A function inside the FC');
register(aFunction);
}, [register]);
return <p>Child</p>;
}
function Parent() {
const childCallback = useRef();
const register = useCallback((callback) => {
// I use a ref but could be a state
// if needed to display JSX
childCallback.current = callback;
}, []);
return <Child register={register} />;
}
Well, it works, but it adds some boilerplate which is not the easiest to understand how it works. It’s time to go deep in the subject.
Firstly, I would like to talk about the behavior of ref
with Class component.
When we pass a ref
to a Class component then we get the React element instance.
class ClassComponent extends React.Component {
aFunction = () => console.log('A function inside the CC');
render() {
return <p>A class component</p>;
}
}
function Parent() {
const myRef = useRef();
useEffect(() => {
// Will log the React element instance
console.log(myRef.current);
}, []);
return <ClassComponent ref={myRef} />;
}
It will then log:
Thanks to that, you can call imperatively any method of a Class component child from its parent thanks to the ref
.
function Parent() {
const myRef = useRef();
return (
<>
<ClassComponent ref={myRef} />
<button
type="button"
onClick={() => myRef.current.aFunction()}
>
Executes aFunction
</button>
</>
);
}
If you try to do the same with a Functional child component, you will get the log undefined
.
function FunctionalComponent() {
const aFunction = () =>
console.log('A function inside the FC');
return <p>A functional component</p>;
}
const ForwardedRefFunctionalComponent = React.forwardRef(
FunctionalComponent,
);
function Parent() {
const myRef = useRef();
useEffect(() => {
// It will log `undefined`
console.log(myRef.current);
}, []);
return <ForwardedRefFunctionalComponent ref={myRef} />;
}
function FunctionalComponent({ aRef }) {
const aFunction = () =>
console.log('A function inside the FC');
return <p>A functional component</p>;
}
function Parent() {
const myRef = useRef();
return <ForwardedRefFunctionalComponent aRef={myRef} />;
}
You probably have guessed it, useImperativeHandle
will help you to solve it. The hook allows to expose some method of a child FC to its parent by customizing the passed reference.
Now that we have the purpose of this hook, let’s see how to use it.
It takes 3 parameters:
- the reference to customize
- the APIs to expose as a callback
- an optional array of dependencies (when handler depends on state): has the same behavior than
useEffect
array dependency
useImperativeHandle(ref, createHandle, [deps]);
For example with the the previous example it will be:
function FunctionalComponent(props, ref) {
useImperativeHandle(ref, () => ({
aFunction: () =>
console.log('A function inside the FC'),
}));
return <p>A functional component</p>;
}
And now the log will be:
Amazing, right? :)
Well, now that you know how to use the hook, it’s time to see a real use case where the component API is good, and it’s useful to use useImperativeHandle
.
When displaying a list with a lot of elements, for example like Twitter, you can encounter some layout performances issues. This is where virtualization comes in handy. Basically, only the elements displayed on the screen are present in the DOM (with few element before and after) which makes the DOM much lighter.
To do that you would make a component named VirtualizedList
which will handle virtualization. Behind the hood, there are some calculations in function of the height of rows and handling position to know which rows need to be in the DOM and where they are displayed on the screen.
We would like to be able to scroll to a specific items, in an imperative way. It’s the perfect use case for useImperativeHandle
:
function VirtualizedList(props, ref) {
useImperativeHandle(ref, () => ({
scrollTo: (itemId) => {
// Do some process
},
}));
return (
<ul>
{
// Right items to display
}
</ul>
);
}
Alright, now you have seen you can expose an API from a child to its parent, but do not overuse it. Most of the time you will think you need it, it’s probably that the API of your component is not right.
It’s possible to do it in Class components and in Functional components as well. In FC, do not forget to forwardRef
otherwise it would not work.
You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.