Tinkering with useImperativeHandle
This is a small blog informing about my learnings after tinkering with useImperativeHandle.
One fine day at work, I was asked to implement a fix and while going through the codebase I came across this hook.
Until this point, even though I have been using React for some time now, I was not aware it. 🙂
I started looking into it from none other than react official docs. P.S. they are really well written. So, I won't be repeating same things here, but it's a perfect place to start to learn.
Basics:
So, with ref, you can do three things:
Either pass it down
Or attach it to a DOM(any native platform)
Or useImperativeHandle that consumes it.
If not these, probably ref is not very useful.
Passing it down is simple. You wrap the component with forwardRef and then its the responsibility of the parent(the consumer) to provide the
ref
prop. Ok, now here I also thought why to use forwardRef? I have the option to pass it as a prop directly. That should also be possible, right? It's just a prop at the end. Turns out, yes we can do that, but react docs suggests to use forwardRef for consistency.Attaching a ref to a DOM element is extremely easy. Add it as a value on
ref
attribute on the DOM element. For eg:<input ref={inputRef} />
useImperativeHandle
is used to expose certain methods of a child component to a parent component. It's mostly used along withforwardRef
.
Let's dive a bit into code now and tinker around. I took the example from the official docs itself and extended it.
It's a simple example. A button to submit comment, an input for comment input, and a list of comments rendered in a div of fixed height.
Goal:
- Focus input on load.
- Read the comment input value.
- When the comment is submitted it should get added to the end of the list and it should get scrolled down to that comment.
Focusing input on load is an ancient thing now :).
Since I had anyway attached ref to input for focus, I used the same for reading the input value.
I want to attach 3 different methods on the ref.
- focus: that uses inputRef to focus.
- getComment: uses inputRef to get the value in the input
- scrollToBottom: uses commentListRef attached to div in which comment list is rendered.
So, I created a parent component <CommentDetail/>
that renders both input and comment list and has these refs defined.
In the same component, we use the useImperativeHandle
hook.
const inputRef = React.useRef<HTMLInputElement | null>(null);
const commentListRef = React.useRef<HTMLDivElement | null>(null);
React.useImperativeHandle(
ref,
() => {
return {
focus: () => {
inputRef.current?.focus();
},
scrollToBottom: () => {
commentListRef.current?.scrollTo({
behavior: "smooth",
top: commentListRef.current.scrollHeight,
});
},
getComment: () => {
return inputRef.current?.value;
},
};
},
[]
);
Simple enough, right? The ref is received from props. commentListRef is passed and eventually gets attached to the div(DOM element) in which comment list is rendered. inputRef is passed and eventually gets attached to the comment input DOM element.
Bonus: If you try this code, you will observe there is always a little gap from the bottom when it gets scrolled to bottom after adding the comment. There is a small bug in our code! Will come back to this later!
Ok, hope it was clear till now!
Then, I was thinking why do we really need useImperativeHandle
What is it doing? Basically storing the object in the ref.current
right?
So, if I do something like
ref.current = () => {
return {
focus: () => {
inputRef.current?.focus();
},
scrollToBottom: () => {
commentListRef.current?.scrollTo({
behavior: "smooth",
top: commentListRef.current.scrollHeight,
});
},
getComment: () => {
return inputRef.current?.value;
},
};
}
This too shall get the job done! And yes, it does work! But we broke the convention here. When using a ref, we always assume that current will be an object. In our case, it became a function. Let's refactor it a bit and return an object instead?
ref.current = {
focus: () => {
inputRef.current?.focus();
},
scrollToBottom: () => {
commentListRef.current?.scrollTo({
behavior: "smooth",
top: commentListRef.current.scrollHeight,
});
},
getComment: () => {
return inputRef.current?.value;
},
}
And yes, this works as well. But now, whenever our component re-renders ref.current will be re-assigned. This can cause potential bugs in our code. And we know, refs are(and should be) retained by React between re-renders. While in our case, ref is getting changed between every re-render. Thus whatever we are doing is an anti-pattern and should avoid it.
Then, I thought, waiiiiit, I can wrap it do it inside useEffect. That way, if I optionally need to change ref when some deps
change, I can do that as well!
Absolutely right! And congratulations 🎉 , because what you just did is, you excavated the internals of useImperativeHandle
The hook internally does the same job, then why reinvent the wheel?
If the ref is an object with a current property, it assigns the value returned from the function passed as the second argument to useImperativeHandle to ref.current
function useImperativeHandle(ref, createHandle, deps) {
// Only run this effect when the component mounts and when deps change
useEffect(() => {
if (ref !== null && typeof ref === 'object') {
// If ref is an object with a 'current' property, assign the new handle to it
ref.current = createHandle();
}
}, deps);
}
Ohh, and let's come back to the bug where it does not get scrolled to the bottom!
The reason is the commentListRef.current.scrollHeight
considers the stale value of the height which is the height of the div before the comment was added to the list.
The solution is, we need to make sure that the height is calculated after the comment is added and rendered in the div.
So, we wrap it inside setTimeout(...,0)
. This ensures that everything is rendered before calculating the height of the container, since whatever we do inside setTimeout will be executed only after call stack is cleared and event queue is empty.
Here is the final working demo of the same.
Hope, I was able to add some value. Thank you for your time. If you think, there is something I have missed or maybe you find few corrections in my understanding , feel free to comment! Always, open to learn!
Thank you🙌. Peace✌️
Subscribe to my newsletter
Read articles from Prajwal Vishal Jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by