Forwarding Refs in React

What's a Ref?

Put simply, a ref in React is a reference to anything, and whilst that may sound a bit vague, it allows you to access an inner object of a component and perform actions.

Consider that you've got a HTML <input> element and that you wish to call the focus() method - you'd need access to the <input> instance.

Improperly done, this may result in lookups using querySelector or it's kin, however react provides refs for exactly this purpose.

tsx
import {useRef, useCallback} from 'react';

export const MyForm = () => {
    const ref = useRef<HTMLInputElement>(null);

    const handleClick = useCallback(() => {
        ref?.current.focus();
    }, [ref]);

    return (
        <div>
            <label for="name">Name</label>
            <input id="name" type="text" ref="ref"/>
            <button onClick={handleClick}>Focus</button>
        </div>
    );
}

In our component above, we're using useRef to say "create a reference, initially it's null, it's going to be a HTMLInputElement".

We're then passing that into the input, when the input is mounted on the page the ref's ref.current value becomes a pointer to that HTML input element.

Then in the handleClick callback we're simply calling focus on it if it's not null.


I thought this was about forwarding refs?

So there are times, when you're building custom component's that you may want to forward a ref, such as if you're building a component which wraps a HTML element, or wrapping a 3rd party library component - as that's when you're going to need a forwardRef.

If we talk about a real world example, we can use the NextJS <Link> to dig into when we'd want to forward our refs. NextJS don't allow you do pass className into their Link component, and they instead expect you to pass a HTML <a> tag into the Link component and set passHrefon the Link.

tsx
export const MyPage = () => (
    <Link to="/" passHref>
        <a className="my-link">
            Hello World
        </a>
    </Link>
}

The NextJS Link component is then iterating through the children, anted passing a ref in, and hooking onto events that happen on hover, intersection and click.

It's pretty awesome that it does this, but it doesn't massively help us here, as every time we create an <a> tag we have to pass our className in again.


Building an Anchor component

Let's look at how we can create a reusable <Anchor> component (yes, anchor, that's what the A stands for in <a>).

We'll start off with a simple scaffolding:

anchor.tsx
tsx
import { ComponentProps, FC, ReactNode } from 'react';
import clsx from 'clsx';

export interface AnchorProps extends ComponentProps<'a'> {
    children: ReactNode;
    variant: 'primary' | 'secondary';
}

export const Anchor: FC<AnchorProps> = ({children, variant, ...props}) => (
    <a 
        className={clsx({
            'button': true,
            'button--primary': variant === 'primary',
            'button--secondary': variant === 'secondary',
        })} 
        {...props}
    >
        {children}
    </a>
);

This somewhat simple Anchor component allows us to pass in whether it's a primary or a secondary (when was the last time we used an app with only one style of link anyway), and children with all other props being spread onto the <a> tag - however this isn't quite enough to satisfy NextJS's router, as well as anyone wanting to say... call focus on it. For that we're going to need a forwardRef.

anchor.tsx
tsx
import { ComponentPropsWithoutRef, FC, ReactNode } from "react";
import clsx from "clsx";

export interface AnchorProps extends ComponentPropsWithoutRef<"a"> {
  children: ReactNode;
  variant: "primary" | "secondary";
}

export const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
    ({ children, variant, ...props }, ref) => (
      <a
        className={clsx({
          "button": true,
          "button--primary": variant === "primary",
          "button--secondary": variant === "secondary"
        })}
        ref={ref}
        {...props}
      >
        {children}
      </a>
    ));

Thankfully React helps us out here somewhat, lets start with the typing - instead of using ComponentProps we can now use ComonentPropsWithoutRef which is basically the same as Omit<ComponentProps, 'ref'>.

Next we can look into our component function, it's no longer implementing FC (although I suspect in React 18 they've fixed the typing issue which prevents this), it's now equalling the result of a new function called forwardRef, to which we can pass our component props, and the interface for the ref it's self: forwardRef<HTMLAnchorElement, AnchorProps>.

Then to the forwardRef function we pass in our component function, which instead of simply receiving it's props, it's also now receiving a second parameter which fully typed instance of our ref.

We can now take this and pass it to our inner HTML <a> tag. It's that simple! How crazy is that.


Putting it all together

Now we've finished building our Anchor component we're able to plug this "bad boy" into the NextJS Link component which will now use our forwarded ref - giving us the integration we need, along with the reuse which we love.

tsx
export const MyPage = () => (
    <Link to="/" passHref>
        <Anchor>
            Hello World
        </Anchor>
    </Link>
}

What's next?

There's loads more you can look at from here, for example if you wanted to create a custom ref for a component you're authoring you could use React's useImperativeHandle.

Things get a bit more complicated when you want to do stuff with the ref too, however the fantastic React Use library has you covered here with it's useEnsuredForwardedRef hooks.

Checkout the fantastic React Use Library on GitHub

streamich/react-use