24-Dec

React, Development

What's the deal with useRef and forwardRef in React?

You might have encountered useRef and forwardRef before, but not quite grasped what they are about. In this short article, I remove the mystery and provide some simple, useful examples of what they can be used for.

3 min read

Β·

By Marcus Haaland

Β·

December 24, 2023

Let's start with something you're probably familiar with. Take a look at the search field above. In Vanilla JavaScript, how would you set focus on an element as soon as you enter a website?

A common approach would be to use document.querySelector:

<!-- input.html -->
<input id="input">

<script>
window.onload = function() {
  // πŸ‘‡ Putting reference to the elemtent in a variable
  const inputElement = document.querySelector('#input');

  // πŸ‘‡ Using the reference to set focus
  if (inputElement) {
    inputElement.focus();
  }
};
</script>

But if you use document directly in React, you bypass the virtual DOM, which can lead to synchronization problems and make the code less predictable.

So, is there something equivalent to document.querySelector in React?

How useRef Works

useRef in React is a hook that can provide the same functionality as querySelector, but in a way that is compatible with React's way of handling the DOM.

Here's how the same behavior is implemented with useRef:

import React, { useRef, useEffect } from "react";

const InputFocusComponent = () => {
  // πŸ‘‡ ref starts as null, but gets a value as an input element
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);
  
  // πŸ‘‡ ref is coupled to the element
  return <input ref={inputRef} />;
}

As you can see from the type, useRef here refers to an input element. But we start the value with null, because it doesn't get a value until the element has been rendered in the document.

The reference to the element provides access to all its values and functions:

An element has a long list of values ​​and functions available
An element has a long list of values ​​and functions available

... and that includes setting focus:

inputRef.current.focus();

Why Do You Need forwardRef?

useRef works fine on HTML elements, but when you try to reference components, problems arise:

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

  useEffect(() => {
    // πŸ‘‡ ref.current will always be null
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  // πŸ‘‡ ref on a component doesn't work off the bat
  return <CustomInput ref={inputRef} />;
}

The problem is that ref is a reserved word and will therefore not be available as part of props.

This is warned about in the log:

Setting ref on components without forwardRef fails
Setting ref on components without forwardRef fails

The log not only gives us a red obstacle but also a solution. When we use ref on an element that is a component, we must enclose the component with the higher-order component forwardRef.

Note also that forwardRef<> accepts two types of arguments: the type of the element and the typing for the properties (props).

import React, { useRef, useEffect, forwardRef } from "react";

// πŸ‘‡ Wrapping CustomInput in forwardRef
const CustomInput = forwardRef<
  HTMLInputElement, // πŸ‘ˆ Typing the element
  React.InputHTMLAttributes<HTMLInputElement> // πŸ‘ˆ Typing the props
>((props, ref) => {
  return <input ref={ref} {...props} />;
});

forwardRef is very useful for being able to control your components, whether it's for your own sake or a library you offer.

What Else Can You Use useRef For?

There is a special trait with useRef, which means that it's not only useful for referring to HTML elements. The trait is that useRef starts its life as an object, {current: null}. This means that the value we care about, current, can be changed without re-rendering the entire component.

This makes it possible to use useRef to store values across re-renders. These values do not have to be HTML elements, which means you can use useRef for a variety of applications. For example, you can refer to time intervals to implement debouncing or count changes across renders.

Here is a simple example of the latter. With each input, a re-render will occur. And the re-render will increment the value for the ref:

function CountEveryRender() {
  const [inputValue, setInputValue] = useState<string>("");
  const count = useRef<number>(0);

  // πŸ‘‡ Increments every render
  count.current = count.current + 1;

  return (
    <>
      <input
        onChange={(e) => setInputValue(e.target.value)}
        value={inputValue}
      />
      <p>Render Count: {count.current}</p>
    </>
  );
}

This is what it looks like:

Writing in input increments counter
useRef can be used to keep track of a value across renders

Concluding Words

I hope this post helped you understand a bit more about what useRef and forwardRef are trying to solve, and how they work in everyday challenges.

If you want to read more about correct typing of ref, I recommend Matt Pocock's simple approach with ElementRef: https://www.totaltypescript.com/strongly-type-useref-with-elementref