Overview
This chapter will introduce you to how to use references in React. You will be able to apply the knowledge gained from the chapter to implement different ways of applying references. By the end of this chapter, you will be able to use Refs in your existing React application in an effective way.
In the previous chapters, we learned about the various methods we can use to fetch data from external APIs and how to include that data inside your React application. This process required you to send XMLHttpRequest(XHR) requests over the network, take the result, and store it in your component.
A web page is often composed of more than just a React app; for example, an application can be embedded in a website where there are various elements in a web page (including our React app) all working together, like different form elements or HTML components. With all these elements that live outside your React components, you might think: could we access these other elements that are not controlled by the React application itself?
This question leads to the following section. We actually can control these elements "living outside" of a React application through References(Refs).
In React, we use Refs to access, manipulate, and interact with the DOM directly. Direct references to HTML elements allow us to perform tasks that are commonly encountered when creating client-side applications. Those tasks include, but are not limited to:
In this chapter, you will learn about the usage of Refs, different methods for how and where to apply them, and finally, how to abstract and hide their implementation details.
In order to understand the purpose of using React Refs, we have to first understand what DOM elements are and the meaning of references in relation to them.
For JavaScript to interact with and manipulate a DOM element, it needs a reference to that element. This reference is simply an object representation of a DOM element. Through the properties of the reference, we can read and write to the element's attributes, which, in turn, define its appearance and behavior in the browser.
You might wonder why we would want to manipulate a DOM node directly if we are already creating these using React in the first place. Refs can and should be used, when either the element that we wish to handle was created outside our app's scope or when the React element's properties do not suffice to achieve a specific goal.
Let's take a look at one of the scenarios that cannot be achieved with the properties provided by the React element as the first exercise of this chapter. In the following exercise, we will look into creating custom upload buttons.
In this exercise, we will use a Ref to create a styled input button, which could not be styled using plain CSS. We will first create the file input and then add a custom button component that will trigger the file upload functionality of the file input when it is clicked. To do that, let's perform the following steps:
$ npx create-react-app upload-button
$ cd upload-button/ && npm start
import React from "react";
class App extends React.Component {
render() {
return <input />;
}
}
export default App;
import React from "react";
class App extends React.Component {
render() {
return <input type="file" />;
}
}
export default App;
When we run the application at http://localhost:3000, we should see the following output:
If you click this input button, you should be prompted by the browser to select a file to upload.
class App extends React.Component {
render() {
return (
<div>
<input type="file" />
<button style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
);
}
}
Note that you have to wrap the input and button components inside a div element, because each React component is required to have a single root sub-component. This div element is only used to display the buttons in the view layer.
class App extends React.Component {
render() {
return (
<div>
<input type="file" hidden={true} />
<button style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
);
}
}
To make the button component trigger the upload prompt, you have to create a Ref to the input component and then trigger the native click function on this reference.
class App extends React.Component {
inputRef;
render() {
return (
<div>
<input type="file" hidden={true} />
<button style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
);
}
}
class App extends React.Component {
inputRef;
render() {
return (
<div>
<input
ref={refParam => this.inputRef = refParam}
type="file" hidden={true}
/>
<button style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
);
}
}
class App extends React.Component {
inputRef;
render() {
return (
<div>
<input
ref={refParam => this.inputRef = refParam}
type="file" hidden={true}
/>
<button
onClick={() => this.inputRef.click()}
style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
);
}
}
The output is as follows:
Now, click the button component and you will be prompted with the file upload window, because the custom button now triggers the file input's native functionality.
As you can observe, you have utilized a React Ref to bind the functionality of a native input that is hardly customizable to a button component that you can style as you wish. Now, after this first success, we should have a closer look at what you have just done. In particular, we will inspect the function that was passed as the ref object to the input component.
Now that we have practiced using Refs in a hands-on exercise, let's take a look at the various ways of creating Refs.
There are thee approaches to creating React Refs:
The first approach is used on class-based components where you can save the reference to a class field. In the previous exercise, we followed this approach when we created a Ref by passing a callback function as the reference to an input element:
<input ref={refParam => this.inputRef = refParam} />
This works because the ref object is automatically called by React when rendering and passes a reference to the created DOM element back into this function. We then persisted this reference into a class field called inputRef, so that it is accessible everywhere in the component's instance.
As it turns out, the second way of creating a Ref also relies on class-based components. Unlike the callback type, here, the ref object is not only passed as an argument to the callback function but can also be initialized beforehand and can be populated as soon as React renders the component. To implement this manner, React provides a utility function called createRef. This method creates a reference that we can assign to a class field and pass it to the component we want to access though the ref. The following code snippet shows how a ref is created:
class App extends React.Component {
inputRef = React.createRef();
render() {
return (
<input ref={this.inputRef} />
);
}
}
As we can see from the preceding code, the return value of the React.createRef function is an object with a single key called current. This current key, when initialized, is either empty or contains the values that were passed to the createRef function. The value of the current key will be replaced with the reference to the selected DOM element.
This would require us to change the code from the previous exercise to not calling the native click function on the Ref directly, but rather on the Ref.current object, as shown in the following code:
<button
onClick={() => this.inputRef.current.click()}
style={{ backgroundColor: "gray", color: "white" }}
>
upload document
</button>
The last approach to creating React Refs is by using a React hook and can therefore only be used with functional components.
In our existing code example, we could restructure our class-based component to a function and leverage the useRef hook to achieve the same result of a button triggering another DOM element's functionality through a reference:
const App = () => {
const inputRef = React.useRef();
return(
<div>
<input ref={inputRef} type="file" hidden={true} />
<button
onClick={() => inputRef.current.click()}
style={{ backgroundColor: "gray", color: "white" }}>
upload document
</button>
</div>
)
}
The result of applying this useRef hook is very similar to the createRef utility function. That is, the return value is the same, namely, an object with only one key called current. And the value of current is either null or the value that was passed as a parameter to the useRef hook. Yet there is one key difference that makes the useRef hook more suitable for functional components; useRef, as opposed to createRef, preserves the reference across renders. This means that even if the function rendering a component is executed multiple times, the useRef hook always returns the same Ref prop that was created initially, regardless of how often the hook was triggered. All we did in the preceding code snippet was change the class syntax to a function syntax and use the React.useRef hook instead of the React. createRef function to initialize our ref.
Since we have explored the three ways to create references to DOM elements in our applications, we should go on and practice each of the approaches in an exercise.
We can use the class-based components and createRef using a very common use case for accessing HTML elements: to measure them, their height, and width.
In this exercise, we will create a merely visual div element and read its dimensions by creating a reference to this DOM node. Knowing the dimensions, such as the height, width, or offset of an element, comes in handy when this cannot be done using the properties, we access on React components. Therefore, it's necessary to access the DOM elements directly using references to them.
$ npx create-react-app custom-button
$ cd custom-button/ && npm start
import React from "react";
class App extends React.Component {
render() {
return <div />;
}
}
export default App;
import React from "react";
class App extends React.Component {
render() {
return <div
style={{ width: 80, height: 20, border: "4px solid black" }}
/>;
}
}
export default App;
You end up with a black-bordered box on your screen.
import React from "react";
class App extends React.Component {
divRef = React.createRef();
render() {
return <div
style={{ width: 80, height: 20, border: "4px solid black" }}
/>;
}
}
export default App;
import React from "react";
class App extends React.Component {
divRef = React.createRef();
render() {
return <div
ref={this.divRef}
style={{ width: 80, height: 20, border: "4px solid black" }}
/>;
}
}
export default App;
import React from "react";
class App extends React.Component {
divRef = React.createRef();
render() {
return <div
ref={this.divRef}
onClick={() => console.log(
this.divRef.current.getBoundingClientRect()
)}
style={{ width: 80, height: 20, border: "4px solid black" }}
/>;
}
}
export default App;
If you click on the div element, you will see a log to your browser console that looks similar to this one:
DOMRect {x: 8, y: 8, width: 88, height: 28, top: 8, …}
x: 8y: 8
width: 88h
eight: 28
top: 8
right: 96
bottom: 36
left: 8
These are the dimensions of the element in the browser. You might wonder why the height is 28 pixels instead of the 20 pixels that you defined in step 4. This happened because the actual height dimension is the sum of the element height and the height of the borders, which is 4 pixels each for the top and bottom borders. The same is true for all the other dimensions as well.
In this exercise, we used a reference to an element and measured its size and offsets, which otherwise cannot be done without accessing the DOM directly. For this exercise, we applied a class-based component. However, in this component, there is no state to be managed. Therefore, we could also use a functional component and reduce the slight code overhead caused by the class syntax. Let's change the structure in the next exercise.
This exercise will start where the previous exercise left off. Here, you will create a Ref in a functional component and use it to get the dimensions of a DOM element (information that is typically only accessible through the DOM directly). This will demonstrate how we can measure the dimensions of an element using a functional component. Therefore, you will not need to create yet another React application but can continue with the last output of Exercise 17.02, Measuring the Dimensions of a div Element in a Class-Based Component. The ref will give us access to the DOM element for the component, which is what we need to access the size of the rendered element in the browser:
import React from 'react'
const App = () => {
return <div />
}
export default App;
const App = () => {
return <div
style={{ height: 20, width: 80, border: "4px solid black" }}
/>
}
const App = () => {
const divRef = React.useRef();
return(
<div
ref={divRef}
style={{ height: 20, width: 80, border: "4px solid black" }}
/>)
}
const App = () => {
const divRef = React.useRef();
return (
<div
ref={divRef}
onClick={() => console.log( divRef.current.getBoundingClientRect())}
style={{ height: 20, width: 80, border: "4px solid black" }}
/>)
}
When clicking the div element, the exact same dimensions as at the end of Exercise 17.02, Measuring the Dimensions of a div Element in a Class-Based Component will be displayed:
DOMRect {x: 8, y: 8, width: 88, height: 28, top: 8, …}
x: 8
y: 8
width: 88
height: 28
top: 8
right: 96
bottom: 36
left: 8
So far, we have seen how to create and pass Ref when the element that we want to interact with via a reference is created within the render method of the component that also uses this Ref. But what if we wanted to access a reference of a component that is wrapped by a reusable higher-order component (HOC) and this HOC simply adds functionality to its children components by caring about Ref objects? In such a case, we would not want to change the HOC in such a way that it handles the implementation detail of manipulating the reference.
In order to preserve the encapsulation of components, we can use a proxying technique referred to as Forwarding Refs.
In this section, we are going to discuss the methodologies used in order to implement Forwarding Refs. These are as follows:
Composition
One of the key concepts of React is the composition of components. That means we want to be able to encapsulate logic without creating dependencies between components.
Let's look at the following code snippet, in which the SayHello component adds a button that says hello to literally any child component that you pass to it. This way, the child component only needs to know about what it is supposed to know, in the same way the SayHello component only cares about the logic and the UI responsible for saying hello:
import React from "react";
const SayHello = props => {
return (
<div>
{ props.children }
<button onClick={() => console.log("Hello!")}>
Say Hello!
</button>
</div>
)
}
const App = () => {
return (
<SayHello>
<p>I wish I could say hello...</p>
</SayHello>
)
}
export default App;
Following this example, we could replace the paragraph wrapped by the SayHello component with anything else that we want.
However, if we want to access the button component that is created in the SayHello component from outside, we can't do that because the components are composed.
Luckily, React provides a utility function to get hold of the reference inside another component as well. This utility is named React.forwardRef and lets us pass a reference through a proxy to a child component that we wish to interact with.
To proxy a Ref to the SayHello button, we need to first wrap the functional component inside the React.forwardRef utility, as shown in the following code:
const SayHello = React.forwardRef(props => {
return (
<div>
{ props.children }
<button onClick={() => console.log("Hello!")}>
Say Hello!
</button>
</div>
)
})
Unlike all other functional components, forwardRef not only takes a single props parameter, but also a second parameter, which obviously holds the value of the reference we want to pass. We will call this secondary parameter forwardedRef, and pass it to the button component inside the render method of the SayHello component:
const SayHello = React.forwardRef((props, forwardedRef) => {
return (
<div>
{ props.children }
<button
ref={forwardedRef}
onClick={() => console.log("Hello!")}
>
Say Hello!
</button>
</div>
)
})
This way, we would be able to access the Say Hello button from the App component. With this forwarded reference, we could mimic trigger the Say Hello button from another button in the App component, as shown in the following code:
const SayHello = React.forwardRef((props, forwardedRef) => {
return (
<div>
{ props.children }
<button
ref={forwardedRef}
onClick={() => console.log("Hello!")}
>
Say Hello!
</button>
</div>
)
})
const App = () => {
const buttonRef = React.useRef();
return (
<SayHello ref={buttonRef}>
<p>I wish I could say hello...</p>
<button
onClick={() => buttonRef.current.click()}
>
Also say hello!
</button>
</SayHello>
)}
The output is as follows:
As you can see from the above figure, forwarding the Ref from the App component is no different to passing the Ref as a prop, as we have already done in the preceding sections. We can now trigger the onClick handler of the Say Hello! button by clicking the button. It's even possible to pass the reference down multiple levels through multiple React.forwardRef functions.
Let's grab the opportunity to practice the forwarding of Ref objects. To do so, we will refer back to the previous exercise, where we measured the dimensions of an element; only this time, we will proxy the reference through another component.
In this exercise, we will create a custom component that encapsulates the styles of a button component. We will measure its dimensions from outside the custom component using useRef and forwardRef. Let's perform the following steps:
$ npx create-react-app proxy-styles
$ cd proxy-styles/ && npm start
import React from "react";
const StyledButton = props => {
return null;
};
const App = () => {
return null;
};
export default App;
import React from "react";
const StyledButton = props => {
return <button>{props.children}</button>;
};
const App = () => {
return null;
};
export default App;
import React from "react";
const StyledButton = props => {
return <button>{props.children}</button>;
};
const App = () => {
return <StyledButton>This is the button text</StyledButton>;
};
export default App;
import React from "react";
const StyledButton = props => {
return (
<button
style={{ backgroundColor: "grey", color: "white" }}
>
{props.children}
</button>
);
};
const App = () => {
return <StyledButton>This is the button text</StyledButton>;
};
export default App;
const StyledButton = React.forwardRef((props, forwardedRef) => {
return (
<button style={{ backgroundColor: "grey", color: "white" }}>
{props.children}
</button>
);
const StyledButton = React.forwardRef((props, forwardedRef) => {
return (
<button
ref={forwardedRef}
style={{ backgroundColor: "grey", color: "white" }}
>
{props.children}
</button>
);
});
const App = () => {
const buttonRef = React.useRef();
return (
<StyledButton ref={buttonRef}>
This is the button text
</StyledButton>
);
};
const App = () => {
const buttonRef = React.useRef();
React.useEffect(() => {
console.log(buttonRef.current.getBoundingClientRect());
});
return <StyledButton ref={buttonRef}>This is the button text</StyledButton>;
};
Simply running this code in your browser will display the following DOMRect object in the console:
DOMRect
x: 8
y: 9
width: 124.359375
height: 18
top: 9
right: 132.359375
bottom: 27
left: 8
This might be familiar to you, since this a similar output to what you saw in the previous exercises earlier. Specifically, these are the dimensions of the button element inside the StyledButton component. You got hold of these measurements by proxying a reference from the App component to the StyledButton component using the React.forwardRef function.
We will now put the knowledge we have acquired from this chapter to the test and will try our hands at solving the following activity.
Your task will be to create a user-date form that automatically focuses the first input field. You will begin by accessing plain input fields via React Refs. From there, you will move on to create a custom input component that can forward the reference to an input field. In the end, you will encapsulate the logic to auto focus and handle the ref by creating a controllable form component.
The following steps will help you to complete the activity:
Hint: You could replace all native input fields in the App component with the FocusableInput component. Just make sure not to pass the same Ref to all FocusableInput components. By the way, the Ref prop on the FocusableInput component is optional; you don't have to pass it.
The final output is as follows:
Following this activity, you have a controllable component that hides all of the imperative code to handle the focus of an input component depending on whether or not a specific prop is passed to this component. This activity is a perfect use case on how to create clean logic to access DOM elements directly using Refs and even proxy them to abstract the complexity even further.
Note
The solution of this activity can be found on page 755..
In this chapter, we have covered a really interesting part of React. The reality is that Refs are not something you are regularly going to encounter unless you are responsible for creating component libraries or working with more design-focused elements.
It is an interesting part of React because, as you might have noticed, Refs do not really fit into React's declarative style. Every time we use Refs, we use them to tell the browser how to do things instead of what we want to see. While not necessarily a problem, it can get complicated and we need to be clear about our intent when using Refs.
The best we can do is to hide most of what we are doing with Refs and ensure that anyone using our code only interacts with our components via props. If we need to access details more closely related to how the browser renders the component (such as focus, scrolling, or dimensions), that will require Refs because React can't handle it without them, but ultimately, the consumer of our code should not be aware of how we achieved these things. In the next chapter, we will look at Refs in more detail.