Skip to content

Instantly share code, notes, and snippets.

@justincy
Created March 31, 2020 21:26
Show Gist options
  • Save justincy/00f86802c4da9ab1733f5b9291292767 to your computer and use it in GitHub Desktop.
Save justincy/00f86802c4da9ab1733f5b9291292767 to your computer and use it in GitHub Desktop.
Using React.forwardRef() and an HOC on the same component

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment