React Forwarding Refs

Chapter 25 18 mins

Learning outcomes:

  1. What is meant by forwarding refs
  2. The forwardRef() function
  3. An example of forwarding refs
  4. React Developer Tools and forward ref components
  5. The useImperativeHandle() hook

Introduction

Back in the React Refs chapter from the Foundation unit, we learnt about refs in React and why they are used in applications — that is, mainly to retrieve instances of DOM nodes and directly interact with them from within React components.

Refs are verily an important part of all but the simplest of React apps. Requiring access to actual DOM nodes is a common task that our apps are bound to fall into sooner than we think.

And in such cases, we just ought to create a ref using the useRef() hook and provide it to the component instance representing a DOM element in order to gain access to that instance's underlying DOM node. That's it.

However, this won't always be the case. Sometimes, we may want to provide refs to outer user-defined components and get them to relay access to inner host components via those refs. In such cases, we can't keep on using refs as we've learnt uptil now.

Instead, we have to use a technique called forwarding refs in order to gain access to deeper components (representing DOM elements) from outer components.

In this chapter we get to explore what exactly is forwarding refs in React, how to forward refs using the forwardRef() function, what is the purpose of the useImperativeHandle() hook, and much more.

Without further ado, let's get learning.

What is forwarding refs?

Let's start by understanding what exactly is meant by forwarding refs.

Forwarding refs is a technique in React whereby a component forwards a ref provided to it to a child component.

The child component may either itself forward that ref, if it's a user-defined component, or it may consume the ref, if it's a host component (i.e. representing a DOM element).

Before we proceed forward, it's worthwhile understanding why we even need to forward refs in the first place. Can't we just do without forwarding refs?

Well, when using functional components, which has become the norm since the inception of hooks in React, it's invalid to instantiate one if we provide a ref to it.

Here's an example:

function App() {
   const ref = useRef();
   return (
      <FunctionalComponent ref={ref} />
   );
}

This code will raise a warning in the console that reads something along the following lines:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

It's invalid to provide a ref to a functional component. If we really have to do so, we have to wrap the functional component with the forwardRef() function, which we explore in the next section.

Anyways, this leads to another genuine question: why is this so?

That is, why does React issue a warning when we provide a ref to a functional component? Why can't this be valid?

Well, this has to do with perhaps one of the most painful things in software development — backwards-compatibility.

If you would like to read more on this, you can go through the following snippet, or else skip ahead to the next section to explore the forwardRef() function.

Why do functional components not receive ref directly as an argument?

Back in the day, React had components in the form of classes and they were allowed to accept refs when instantiated via component instances, as follows:

class ClassComponent extends React.Component {
   // ...
}
// Perfectly valid
<ClassComponent ref={ref}>

However, functional components weren't allowed to receive refs, probably because there isn't anything such as instances of functions; classes have instances and so each class's instance could store additional data, but the same isn't the case with functions.

Hence the following code would fail:

// Not valid.
<FunctionalComponent ref={ref}>

As React evolved, people felt the need of allowing function components to accept a ref as well, and thus came into existence the forwardRef() function (which we shall explore below).

Honestly, we feel that some better design decisions could've been made by React here, but nonetheless, what matters is the status quo, not the ideal situation, so let's learn more about forwardRef().

The forwardRef() function

If we wish to provide a ref to a user-defined component with the hope that the component would be able to delegate that ref down to one of its children components, we have to use forwardRef().

forwardRef() is a function exported by the react package (just like the other exported functions, such as useState()).

forwardRef() serves to create a new component, known as a forward ref component, that is capable of accepting a ref.

Here's the simple syntax of the function:

forwardRef(component)

The functional component where you wish to use the given ref is provided to forwardRef() as an argument. Note that this component accepts two parameters: as before, the first one is the props object, while the second ref parameter holds the passed on ref.

In the end, forwardRef() returns a new component that can accept a ref.

Simple, isn't it?

Let's consider an example to help clarify the mist around forwarding refs.

An example of forwarding refs

Let's say we have a component to wrap around a normal HTML <input> element, called TextInput:

function TextInput({ value: initialValue }) {
   const [value, setValue] = useState(initialValue);
   return (
      <input type="text" value={value} onChange={e => setValue(e.target.value)} />
   );
}

As we stated in the React Components chapter, having such a component isn't impractical in real-world React apps — in fact, it's more than just common, as a means of unifying all the logic of an interface element in one single location.

Ought to make all text inputs have the class "input-text" instead of "text-input"? No problem, just go over to the definition of TextInput and change it.

It's that simple.

Anyways, with this component in place, let's now suppose that we have a basic App component using it, along with a button to help place the focus on the input:

function App() {
   return (
      <>
         <TextInput value="Hello World!" />
         <button>Focus on input</button>
      </>
   );
}

Now this is really interesting. The button wants to place focus on the input field, and this requires access to the DOM node representing the input field, yet we only have access to TextInput.

What to do now?

Well, we've just learnt about forwarding refs a while ago and so don't need to worry at all! We need to use forwardRef().

First things first, let's convert TextInput from a normal component into a forward ref component:

const TextInput = forwardRef(({ value: initialValue }, ref) => {
   const [value, setValue] = useState(initialValue);
   return (
      <input ref={ref} type="text" value={value} onChange={e => setValue(e.target.value)} />
   );
});

The ref parameter has to be provided to the child <input> element.

Going back to the App component, now we just need to create a ref and pass it on to <TextInput> (which will delegate it forward to <input>).

function App() {
   const textInputRef = useRef();
   return (
      <>
         <TextInput ref={textInputRef} value="Hello World!" />
         <button onClick={() => textInputRef.current.focus()}>Focus on input</button>
      </>
   );
}

Let's try running this app and see how everything goes:

Live Example

As soon as the button is clicked, the input field receives focus, all thanks to forwarding the ref given to TextInput to the underlying <input>.

React Developer Tools and forward refs

When we create a component using forwardRef(), the component shows up in React Developer Tools with the tag Forward Ref.

Let's see an example.

Shown as follows is the component tree view in React Developer Tools of the application that we created above:

Component tree in React Developer Tools for the app created above
Component tree in React Developer Tools for the app created above

Notice the first child component of App; it reads Anonymous followed by the tag Forward Ref since it has been created using forwardRef().

Whenever forwardRef() creates a component, React Developer Tools annotates the component specially to make it clear as to which components are normal ones and which components are forward ref ones.

In addition to this, if the function provided to forwardRef() is anonymous, the text displayed for the component would actually be 'Anonymous' (as we just saw above). However, if the function has a name, then the name is displayed alongside the tag.

Let's say that TextInput is created as follows, with a named function expression:

const TextInput = forwardRef(function TextInput({ value: initialValue }, ref) {
   const [value, setValue] = useState(initialValue);
   return (
      <input ref={ref} type="text" value={value} onChange={e => setValue(e.target.value)} />
   );
});

This forward ref component would show as follows in React Developer Tools:

Component tree after using a named function
Component tree after using a named function

As can be seen, the component is now named as TextInput, following from the name that we gave to the function provided to forwardRef().

Remember that, in JavaScript, a function expression could have the same name as an existing identifier. This has been detailed in the JavaScript Functions — Basics chapter, from our comprehensive JavaScript course.

The useImperativeHandle() hook

When forwarding a ref, the component that forwards the ref exposes its internal details to an external component. Usually, this ain't an issue but sometimes we might want to be able to control what's exposed to an external component in the instance of ref forwarding.

The useImperativeHandle() hook was designed exactly to target this use case. Let's see how it works.

useImperativeHandle() manually specifies the value assigned to a forwarded ref inside a component.

The name 'imperative' might come from the fact that useImperativeHandle() deals with imperative behavior, such as focusing an input field, scrolling to an element in the document, etc.

Similarly, the name 'handle' might come from the fact that it deals with the value assigned to a ref, which can be referred to as a 'handle'.

Anyways, here's the syntax of useImperativeHandle():

useImperativeHandle(ref, callback, dependencies)

The first argument is the forwarded ref, the second argument is a callback function returning a handle to assign to this ref, and the third argument is a dependency array to use to check whether the callback needs to be re-run or the cached value used to resolve the ref.

Let's consider a quick and simple example

Suppose we want to expose only one method on the ref provided to TextInput, and that is focus(). This focus() method should itself fire the focus() method of the underlying <input> element (using another ref).

Because we need to modify the handle assigned to the provided ref, we seek the usage of useImperativeHandle():

const TextInput = forwardRef(({ value: initialValue }, ref) => {
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus()
}
}
}, []); const [value, setValue] = useState(initialValue);
const inputRef = useRef(); return ( <input ref={inputRef} type="text" value={value} onChange={e => setValue(e.target.value)} /> ); });

The handle returned by useImperativeHandle(), which is ultimately assigned to the current property of ref, contains only one method, focus(), which calls the focus() method of the <input> element.

Notice how in order to obtain access to the <input> element, we employ a second ref, inputRef. Without this second ref, there's no way to access the <input>.

Now inside App, the textInputRef ref will only have one method available to it and that is...you guessed it right...focus(). This can be confirmed by a simple log:

function App() {
   const textInputRef = useRef();
   return (
      <>
         <TextInput ref={textInputRef} value="Hello World!" />
         <button onClick={() => {
console.log(textInputRef); textInputRef.current.focus(); }}>Focus on input</button> </> ); }

The moment the button is clicked, here's what the console shows for textInputRef:

{ current: { focus(): ƒ } }

It's an object with a property current which itself holds a mere object with one method, focus() — just as we expected.

Live Example

See how using useImperativeHandle() in this program, we were able to control the handle exposed by TextInput to the external component App.

In some cases, this might be really useful when we want to provide a limited set of functionality to an external component from a given component.

Why is useImperativeHandle()'s second argument a function?

It's quite apparent why useImperativeHandle() requires a function as its second argument to return a handle and doesn't work with an object directly provided to it as the handle.

Can you reason why? Well, it's so that when the handle is to be created, we have access to all the existing refs inside the underlying component. Simple.