How to useRef and forwardRef in React
What do I do when my component uses a ref internally but also needs to forward a ref from its parent? The short answer is merge the refs into a single ref function. If what that means is not immediatly obvious, you’re like me. Read on.
What is a ref?
Look at what get’s returned from useRef.
const ref = useRef()
console.log(ref)
/* prints */
{current: undefined}
This ref is an object with one property current that can be set to whatever you want.
What happens when I pass it to an HTML element?
<button ref={ref} />
For years I just thought this was magic, but today I grepped “ref” in my node_modules/react-dom
folder and it’s not complicated. After react-dom creates the HTML button instance, it will attach the ref like so:
if (typeof ref === 'function') {
ref(instanceToUse)
} else {
ref.current = instanceToUse
}
}
The ref prop will only accept three things:
- a function
- a plain object with a single current property
- null
This means these two lines result in the same thing.
<button ref={ref} />
<button ref={(instance) => ref.current = instance} />
What about forwardRef?
When in doubt, console.log it out.
const MyButton = forwardRef(function(props, ref) {
console.log(ref)
...
})
What gets printed depends on the ref value given to the component. If you create that MyButton component without passing it a ref, you’ll see null
printed to the console.
<MyButton /> // with no ref
/* prints */
null;
Now let’s try it with a ref from useRef.
const ref = useRef()
<MyButton ref={ref} />
/* prints */
{current: undefined}
Ok, this is starting to make sense. Whatever you pass as the ref prop to the MyButton component will be the same as second argument in the forwardRef function.
<MyButton ref={ref} /> // this ref is the same as...
forwardRef(function(props, ref) {}) // ...this ref
The name “forwardRef” is starting to make a lot of sense. Let’s test this out by passing a function as the ref property.
const func = (node) => console.log("Hi")
<MyButton ref={func} />
/* prints */
'(node) => console.log("Hi")'
/* the string representation of the function */
Merging Two Refs
So to deliver on the promise of this blog’s title, here’s how we can have useRef and forwardRef in the same function component.
const MyButton = forwardRef(function(props, forwardedRef) {
const localRef = useRef()
return (
<button
ref={(instance) => {
// first we set the local ref
localRef.current = instance
// then we handle the forwarded ref
// it can be a function, an object, or null
if (typeof forwardedRef === "function") {
forwardedRef(instance)
} else if (typeof forwardedRef === "object") {
forwardedRef.current = instance
}
}}
/>
);
});
This uses a function to properly set both the local and the forwarded refs.
At this point, you probably can’t help yourself from writing this mergeRefs function.
function mergeRefs(...refs) {
return (instance) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(instance)
} else if (ref != null) {
ref.current = instance
}
})
}
}
To be used like this:
const MyButton = forwardRef(function(props, forwardedRef) {
const localRef = useRef()
return <button ref={mergeRefs(localRef, forwardedRef)} />
});
Turns out, Greg Bergé has created a utility that does just this called react-merge-refs. That mergeRefs function above is almost copied strait from his code. Thanks Greg.
So that’s how you do it.
Comments
Please email your thoughts to my personal address. kerrto-prevent-spam@hto-prevent-spamey.comto-prevent-spam
I would love to hear from you.