Using refs in React functional components (part 2) - forwardRef + useImperativeHandle

carlosrafael22

Rafael Leitão

Posted on January 10, 2021

Using refs in React functional components (part 2) - forwardRef + useImperativeHandle

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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]} />
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 useImperativeHandleeven 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

💖 💪 🙅 🚩
carlosrafael22
Rafael Leitão

Posted on January 10, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related