What are refs?
Occassionally, while building React apps, we might want to store some non-state data on component instances. That is, the data that we want to store doesn't necessitate a rerender of the component instance.
For example, suppose we want to store the actual DOM element associated with a component instance so that we could directly access it and possibly perform some DOM methods on it.
A classic example of this is to focus an <input>
element after it is rendered in React, which is done by calling the focus()
method on the DOM <input>
element node.
In such cases, we obviously don't need to use the state to store this data. What we need instead is a ref.
As you can guess, the term 'ref' is a fancy shorthand for 'reference'. By that means, a ref is simply a reference, i.e. an object, holding non-state data for a given component instance.
Because refs aren't related to the state of components, they don't cause any rerenders.
Now, let's see how to create and work with refs in React, using the useRef()
hook.
The useRef()
hook
Just like the useState()
hook gets a component to be hooked into React's state utility, the useRef()
hook gets it to hook into React's ref utility.
Essentially, calling useRef()
(inside a component) creates a new ref. The hook function can be provided an initial value for the ref.
useRef(initialValue)
useRef()
returns back the underlying ref, i.e. a reference to an object storing the current ref data.
This object isn't the data; instead, it has a current
property which holds the ref data that we provided while calling useRef()
.
What's the point of the current
property on refs?
If you think about it, refs in React can't first of all be primitives.
This is because primitives are passed on by value, not by reference, and are immutable in nature, which means that a primitive can't be modified; only replaced with a new value.
So this leaves us with the fact that a ref can only be an object.
Now if it is an object, the next question is that why is there a current
property on it? Why can't we create just about any property to store our data?
To answer the latter, we can surely create any property on a ref besides current
, but usually there's no need to.
Now to answer the former, the current
property is created by React just for the sake of creating some property where the data of the ref can be stored — we can't just dump the data in the object as it is!
If React wanted to, it could've called this property as data
, or maybe value
, but even if it did so, it would've had to make sure that it remains consistent in this approach. So current
has nothing to do more than the fact that it's a property that the React team decided to use on refs to hold ref data.
A simple example
Let's say we have a program that randomly selects a programming language from an array of languages and then renders it inside a <p>
element. A button serves to get the selection triggered.
Here's the code of the program:
const languages = ['Scala', 'Java', 'Ruby', 'Perl', 'C#', 'Swift'];
function LanguageItem({children}) {
return (
<li>{children}</li>
);
}
function App() {
const [list, setList] = useState([]);
function onClick() {
const index = Math.floor(Math.random() * languages.length);
setList([...list, languages[index]]);
}
return (
<>
<ol>
{list.map(item => (
<LanguageItem>{item}</LanguageItem>
))}
</ol>
<button onClick={onClick}>Add new language randomly</button>
</>
);
}
It might seem intimidating, but it's really simple to understand. All the concepts that you've learnt so far are being applied all together in one combo.
Now, let's suppose that we want to save the time at which a particular selection was made and then showcase it when the selection is clicked by means of alerting it.
Thanks to refs, this is extremely easy to do:
function LanguageItem({children}) {
const createdAt = useRef(new Date());
return (
<li onClick={() => alert(createdAt.current)}>{children}</li>
);
}
The moment a <LanguageItem>
is created for the very first time, the time is recorded at that instant and then stored in a created ref. Thereafter, the ref remains as it is, i.e. it's not updated.
Remember that this same task could've been accomplished by using state, but there's absolutely no need to. If we don't need state, then we just shouldn't use it.
Simple as that.
Similar to how state is preserved between rerenders of React elements, refs are also preserved in the exact same way. This also means that if the element gets thrown away, its refs get thrown away too.
Let's now see how to directly access DOM elements via refs in React.
The ref
prop
Consider the following code:
function App() {
return (
<>
<div>A div</div>
<button>Get dimensions of div</button>
</>
);
}
We want to store a reference to the corresponding DOM element node of the <div>
element here in order to be able to retrieve its width and height later on.
Using refs and our knowledge of the DOM, here's one way to do so:
function App() {
const divElement = useRef(document.querySelector('div'));
function onClick() {
const width = divElement.current.offsetWidth;
const height = divElement.current.offsetHeight;
alert(`Width: ${width}, Height: ${height}`);
}
return (
<>
<div>A div</div>
<button onClick={onClick}>Get dimensions of div</button>
</>
);
}
The document.querySelector()
method selects the <div>
element, which then gets stored in the divElement
ref.
Now, when the button is clicked, we can easily access this <div>
element and retrieve its dimensions, using the divElement
ref.
Although this example works, there's a small issue with it. We had to manually look for the desired DOM element using querySelector()
and then initialized the divElement
using it.
React simplifies this approach, and rightly so, with the help of the ref
prop.
Using the ref
prop, we don't need to manually make any element selections at all!
Let's see how ref
works.
First, as before, we create a ref to store a reference to a DOM element. This obviously means calling useRef()
. Next, we go to the React element whose corresponding DOM element we need to access, and then set a ref
prop on it. The value of this ref
prop is simply the ref that we created before.
Remember that a ref is an object and so passing one to the ref
prop means that createElement()
now has full control of that ref and can therefore modify any of its properties. And that's actually what happens — React takes the ref passed in via the ref
prop of an element and sets the current
property corresponding to the DOM element.
Let's use ref
to reimplement the previous program:
function App() {
const divElement = useRef(); // No need to initialize the ref.
function onClick() {
const width = divElement.current.offsetWidth;
const height = divElement.current.offsetHeight;
alert(`Width: ${width}, Height: ${height}`);
}
return (
<>
<div ref={divElement}>A div</div>
<button onClick={onClick}>Get dimensions of div</button>
</>
);
}
As you can see, this code is much more compact and simpler than the previous one.
Going with this approach of using the ref
prop, we don't need to worry about selecting DOM elements manually — just decide which React element's corresponding DOM node to reference and then pass it a ref
prop.
What could be simpler than that?