Hello everyone! đź‘‹
Continuing the series about refs in functional components, in this article we will cover another case we need refs: when accessing other functional components.
For this article we will understand a bit more about Ref Forwading and useImperativeHandle
, an extra hook that lets us customize the ref the parent component will have access.
If you want to check, I also put the code for these examples on github.
So let's jump into that!
1. Accessing functional components with refs
In all the previous examples, in the first part of this series, we needed to access a DOM element in the same component, but what if we need to access an element from a child component, how would we do that?
1.1 Ref forwarding
As stated in the docs, React components hide their implementation details, including their rendered output. Thus, components cannot easily access refs from their children.
Although this is a good thing, preventing us from relying on other component’s DOM structures, there are cases where we need to access a child’s DOM node for managing focus, selection and animation, for example.
To do that, React provides a feature called Ref Forwarding.
Ref forwarding is an opt-in feature that lets some components take a ref they receive, and pass it further down (in other words, “forward” it) to a child.
To understand it, let's consider a simple example where a parent component wants to have a ref to a child’s input to be able to select its text when clicking on a button:
import React from 'react';
type ForwardedInputProps = {
placeholder?: string
};
const ForwardedInput = React.forwardRef<HTMLInputElement, ForwardedInputProps>(({ placeholder }, ref) => (
<input ref={ref} placeholder={placeholder} />
));
const SimpleForwardRef = () => {
const inputRef = React.useRef<HTMLInputElement>(null);
const selectText = () => {
inputRef.current?.select();
}
return (
<div>
<ForwardedInput ref={inputRef} placeholder="Type here"/>
<button onClick={selectText}>Select text</button>
</div>
);
};
As you can see, we created a ref object with useRef in the parent component and passed it to the child component. In the ForwardedInput
component we call the React.forwardRef
function, which receives props and the ref passed to the functional component and returns the JSX.Element for it.
ForwardedInput
uses the React.forwardRef
to obtain the ref
passed to it, so we can forward the ref down to the DOM input. This way, the parent component can get a ref to the underlying input DOM node and access it through its inputRef
current
property.
One important point to note is the typing in the React.forwardRef
. As a generic function, it receives type parameters for the ref and the props but in the reversed order from its function parameters. Since we attached the forwarded ref to an its type will be HTMLInputElement
.
1.2 useImperativeHandle
In some more advanced cases you may need to have more control over the returned ref the parent will have access to. Instead of returning the DOM element itself, you explicitly define what the return value will be, adding new properties for the returned ref, for example.
In such cases you would need to use a special hook, the useImperativeHandle
. As stated in the docs:
useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef:
Let’s understand it a bit better. Consider the following example where when the user clicks the button associated with the box it scrolls to the top of the box.
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
type BoxProps = {
size: string,
color: string
}
type IncrementedRef = {
getYLocation: () => number | undefined,
current: HTMLDivElement | null
}
const Box = forwardRef<IncrementedRef, BoxProps>(({size, color}, ref) => {
const divRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
getYLocation: () => divRef.current?.getBoundingClientRect().top,
current: divRef.current
}));
return (
<div style={{
height: size,
width: size,
backgroundColor: color,
margin: '0 auto'
}}
ref={divRef}></div>
);
});
const ImperativeHandleExample = () => {
const refs = [useRef<IncrementedRef>(null), useRef<IncrementedRef>(null), useRef<IncrementedRef>(null)];
const goToBox = (position: number) => {
console.log('Go to box: ', refs[position].current?.current)
const boxTop = refs[position].current?.getYLocation();
window.scrollTo({ top: boxTop, behavior: 'smooth'})
}
return (
<>
<div>
<button onClick={() => goToBox(0)}>Go to 1st box</button>
<button onClick={() => goToBox(1)}>Go to 2nd box</button>
<button onClick={() => goToBox(2)}>Go to 3rd box</button>
</div>
<Box size='500px' color='red' ref={refs[0]} />
<Box size='500px' color='blue' ref={refs[1]} />
<Box size='500px' color='green' ref={refs[2]} />
</>
);
};
Here, the Box component is wrapped with a forwardRef
since we are receiving a ref from the parent. But instead of attaching it to the <div>
, we are explicitly changing its return to the parent with the useImperativeHandle
and attaching a new internal ref to the <div>
.
Why so complex? Because we want to provide the ref to the parent component with the coordinate of the top of this <div>
.
Since we want more control over what properties the parent will access from the ref we have the useImperativeHandle
to set this new getYLocation
function and the <div>
as its current
property. The getYLocation
could simply be the value but I put as function to exemplify another way to a have a property.
Remember that with useImperativeHandle
you have to explicitly state what the return of the ref
will be. It won't contain any other property so if you didn't set it as the current
property you wouldn't have access to the <div>
itself in the parent component.
So, in the parent component we create refs and forward it to each Box component. When the user clicks on each button it will call goToBox()
and with its position parameter we get the corresponding ref in the array of refs. Then, with the getYLocation
function we defined with useImperativeHandle
we have the Y coordinate of its top and scroll to it. The console.log prints the <div>
from the ref’s current
property to show that this way we have access to the element.
One last point is the typing again. The ref type passed to the forwardRef function is not a HTMLDivElement
because with the useImperativeHandle
we are creating a new return to be the ref and this new ref
has only the getYLocation
and current
properties.
2. Conclusion
As shown in the above examples, you can also access underlying DOM elements from children functional components with the Ref forwarding
feature. For more advanced cases, you can even customize what the parent component will have access with the ref
passed to its children with the useImperativeHandle
even though, as stated in the docs, imperative code using refs should be avoided in most cases.
If you've came this far, I would really appreciate any feedback or comments pointing any corrections you would suggest. Hopefully this will be helpful to you :)
Also, there is one more article to finish this series where we will see how to use refs in functional components to have something like instance variables. If you want to check that out :)
3. References
This series would not be possible without other articles from awesome developers out there. If you want check what helped my learning, click on the links below:
https://reactjs.org/docs/forwarding-refs.html
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
https://moduscreate.com/blog/everything-you-need-to-know-about-refs-in-react/
https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/
https://www.carlrippon.com/react-forwardref-typescript/
https://stackoverflow.com/questions/57005663/when-to-use-useimperativehandle-uselayouteffect-and-usedebugvalue
https://stackoverflow.com/questions/62040069/react-useimperativehandle-how-to-expose-dom-and-inner-methods
Top comments (3)
Hi Carlos, just wanted to say it was a great post! Thank you for your detailed explanation :)
Hi Tomoaki! Thanks a lot for your message, I'm really happy that it was useful for you :)
Awesome, Thanks for the posting!