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.