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.
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 passHref
on the Link
.
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:
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
.
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.
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