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.
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:
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 ref
s 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 ref
s, 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:
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:
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:
As can be seen, the component is now named as TextInput
, following from the name that we gave to the function provided to forwardRef()
.
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
:
It's an object with a property current
which itself holds a mere object with one method, focus()
— just as we expected.
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.