DEV Community

Cover image for A journey into the world of refs and ref forwarding in React
Nicolas Erny
Nicolas Erny

Posted on

A journey into the world of refs and ref forwarding in React

In many React projects, we need to interact directly with the DOM.
For example, when we want to set the focus to an element. How to do that in React?

Let's start by creating a custom input component.

function MyInput({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
  return <input {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

Now, we want to get the focus after the component is mounted. To do that, we need to get access to the DOM element.
React refs are a way to get a reference to a DOM node. We use the useRef hook to create a ref object called myInputRef. Then, we add the ref to the input element.
myInputRef enables us to call the focus method on the DOM element. As we want to get the focus after it's mounted, we need to use the useEffect hook. We end up with the following code:

function MyInput({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
  const myInputRef = React.useRef<HTMLInputElement>(null);
  React.useEffect(() => {
    if (myInputRef.current) {
      myInputRef.current.focus();
    }
  }, []);
  return <input ref={myInputRef} {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

Imagine that our goal is to create a generic custom component. We do not want to get the focus each time we use our React component. The idea is to be more flexible. We would like to expose a ref and let the parent component decide what to do (set the focus or not).
The naive approach is to add a ref property to MyInput.

function MyInput({
  ref,
  ...props
}: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> ) {
  return <input ref={ref} {...props} />;
}
Enter fullscreen mode Exit fullscreen mode

Here's the App component that uses MyInput:

function App() {
  const inputRef = useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return (
    <div>
      <MyInput ref={inputRef} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It does not work, and we get the following warning:

Warning: MyInput: ref is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

We cannot use a prop called ref, so let's rename it. Here's what the code would be like:

function MyInput({
  myRef,
  ...props
}: React.InputHTMLAttributes<HTMLInputElement> & {
  myRef: React.Ref<HTMLInputElement>;
}) {
  return <input ref={myRef} {...props} />;
}

function App() {
  const inputRef = useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return (
    <div>
      <MyInput myRef={inputRef} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It works. But according to the React documentation, it's not the recommended solution. A component should use the ref property to be compatible with other components and libraries.
Ref is not available in props, so we must wrap the component in forwardRef. Now, our function component accepts ref as the second argument.

Let's refactor our code to use forwardRef instead. Here's the final version:

type MyInputProps = React.DetailedHTMLProps<
  React.InputHTMLAttributes<HTMLInputElement>,
  HTMLInputElement
>;
const MyInput = React.forwardRef<HTMLInputElement, MyInputProps>(function (
  { ...props },
  ref
) {
  return <input ref={ref} {...props} />;
});

function App() {
  const inputRef = useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return (
    <div>
      <MyInput ref={inputRef} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This technique for automatically passing a ref through a component to one of its children is called ref forwarding.

I hope that gives you an idea of a good way to use React forwardRef in Typescript.

Discussion (0)