Focus management in React currently has one solution: refs. If you want a function component to accept a ref, you should use React.forwardRef(). For a basic Input component, it would look like this:
import React from "react";
const Input = React.forwardRef(function Input({name, type, id, disabled, ...props}, ref) {
return (
<input
{...props}
name={name}
id={id}
disabled={disabled}
type={type}
ref={ref} />
);
});
export default Input;
That's great. But what if we wanted to wrap it in an HOC? Maybe we have an HOC, shared between different form controls, for handling status messages. Let's call is withStatusMessages()
. Normally, we would do something like this:
export default withStatusMessages(Input);
Everything will compile, but our ref
stops working and we'll see an error in the console about function components not accepting refs.
What happened?
Remember, the component passed to React.forwardRef()
needs to accept two parameters, with the second one being the ref
. But our HOC doesn't know that, it just accepts and passes on props
. We could update our HOC to pass on refs, but we might want it to be used with components that don't accept refs. So what can we do?
We've already decided that we can't apply the HOC after React.forwardRef()
which means we have to apply the HOC before React.forwardRef()
. We can't just have withStatusMessages(React.forwardRef())
because then our HOC will still drop the ref
and not pass it on. What we need is a way to have the ref
from React.forwardRef()
passed on to the component via props (instead of as a second argument). Here's what I've come up with:
const Input = withStatusMessages(function Input({
name,
type,
id,
disabled,
inputRef,
...props
}) {
return (
<input
{...props}
name={name}
id={id}
disabled={disabled}
type={type}
ref={inputRef}
/>
);
});
export default React.forwardRef((props, ref) => {
return <Input {...props} inputRef={ref} />;
});
We pass in the ref
as the inputRef
prop and then attach it to the input
as normal. Now we get to use both React.forwardRef()
and an HOC on the same component.
Note that we have to rename the prop, we can't just keep ref
. In other words, we can't do this:
export default React.forwardRef((props, ref) => {
return <Input {...props} ref={ref} />;
});
If you do, you'll get the error about function components not accepting refs because ref
is handled specially by React (I don't know why, it sure would be nice if it just worked).
I created a working example of using this technique. And if you use TypeScript, the types are not straightforward so I've got you covered with the same example in TypeScript.
Do you know of a better way to handle this situation? I'd love to hear it.
Top comments (4)
The trick is to also incorporate 'forwardRef' in the HOC.
Awesome quick read about HOC and ref issue. I was going through React documentation on Forwarding refs in HOC but could not understand their example properly. But your example helped a lot.
Thank you Justin for sharing this along with the code sandbox links for both the plain React & in typescript.
Same here, exactly what I needed. Could not find any answers elsewhere and this worked perfectly for me. Thank you!
Exactly what I needed. Great example, thank you for the post :D