In this article, we will discuss some common use cases for React's useRef
Hook.
We will use the Profiler from the React Dev Tools to see how our components are rendering. If you don't have the React Dev Tools and plan to follow along, you'll need to pause and download it now.
If you'd like, you can skim this as a Medium or dev.to article.
fork and clone
cd client
npm i
npm start
As always, we'll start with a tour of our codebase. This time, App.js
is a
simulated Login Form.
We have initialFormValues
, an object with a blank email and password, which we
use to initialize our formValues
state.
const initialFormValues = {
email: '',
password: ''
};
const [formValues, setFormValues] = useState(initialFormValues);
We then have boilerplate handleChange
and handleSubmit
functions, and the
handleSubmit
logs formValues
to the console.
const handleChange = e => {
setFormValues({
...formValues,
[e.target.name]: e.target.value
});
};
const handleSubmit = e => {
e.preventDefault();
console.log(formValues);
};
Finally, our JSX renders the form and assigns the appropriate onChange
and
onSubmit
attributes.
return (
<main>
<section>
<form onSubmit={handleSubmit}>
<label htmlFor='email'>Email</label>
<input
type='email'
name='email'
placeholder='Email'
onChange={handleChange}
/>
<label htmlFor='password'>Password</label>
<input
type='password'
name='password'
placeholder='Password'
onChange={handleChange}
/>
<button type='submit'>Login</button>
</form>
</section>
</main>
);
So you may be asking, "What's wrong with this component? It works as expected, and I've written forms like this countless times."
So have I, friend. So have I.
Open your React Dev Tools and click the Settings cog. Next, click the 'Profiler' tab and tick on the option, "Record why each component rendered while profiling."
Now open the Profiler tab, and you will see a blue dot in the upper left corner. Click it to start profiling. Next, type in the inputs and click the (now red) dot to stop profiling.
Click the App component on the left inside the Profiler
tab, and you'll see a
list of all the renders. Notice the Profiler provides the same reason for each:
Why did this render?
- Hook 1 changed
The component re-rendered on every keystroke.
If our form's sole purpose is to submit the inputs elsewhere, then we don't need the component to re-render in real time. We don't care what the value is while the user is typing. We only care about the value when the form is submitted.
This may seem harmless in a small application, but it can significantly impact our application as it scales. And part of being a good React developer is being mindful of the performance of our applications by minimizing renders.
Enter useRef
In the broadest sense, the useRef
Hook creates a mutable variable that
persists between renders. It provides the ability to store a value we can access
outside the render cycle.
Let's try using useRef
to store our render count.
We'll start by importing useRef
in our current import.
import { useState, useRef } from 'react';
We will initialize renders
with useRef
and set it to 0. When we initialize a
ref
, the Hook creates an object with a current
property. current
is the
property we must use to update the ref
's value.
const rendersRef = useRef(0);
We'll use a useEffect
Hook without the dependency array to increment renders
every time the component renders. And take note that we change the value of a
ref
, unlike useState
, we can do so directly.
import { useState, useRef, useEffect } from 'react';
...
useEffect(() => {
rendersRef.current++;
});
Let's render renders
in our JSX.
<section>
<h3>Render Count: {rendersRef.current}</h3>
<form onSubmit={handleSubmit}>
When we type in the input, we see our render count incrementing in real-time. Neat, I guess, but not particularly useful.
From the React Docs:
useRef
returns a ref object with a singlecurrent
property initially set to the initial value you provided.On the next renders,
useRef
will return the same object. You can change itscurrent
property to store information and read it later. This might remind you of state, but there is an important difference.Changing a ref does not trigger a re-render. This means refs are perfect for storing information that doesn’t affect the visual output of your component.
...information that doesn’t affect the visual output of your component.
Kind of like a login form?
Let's start by creating two new refs
to store our email
and password
values and initialize them as null
.
const emailRef = useRef(null);
const passwordRef = useRef(null);
When using the useRef
Hook to reference a DOM element, associating it is
incredibly simple. All we need to do is add a ref
attribute to the element and
provide it our ref
variable as its value.
<input
type='email'
name='email'
placeholder='Email'
ref={emailRef}
onChange={handleChange}
/>
...
<input
type='password'
name='password'
placeholder='Password'
ref={passwordRef}
onChange={handleChange}
/>
Next, we need to refactor our handleSubmit
function. Instead of logging
formValues
, we'll create an object, setting the values as each ref
's
current
property.
const handleSubmit = (e) => {
e.preventDefault();
console.log({
email: emailRef.current.value
password: passwordRef.current.value
});
};
Type in the inputs and click Login
. You should see your object logged properly
while the render count remains 0
. The Profiler also finds no activity.
Changing the value of our ref
s did not cause the component to re-render.
With that working, we no longer need the following:
- email
onChange
attribute - password
onChange
attribute handleChange
functionformValues
stateinitialFormValues
objectuseState
import
Though we aren't directly using it anymore, we should keep the name
attributes for accessibility.
Test the application again. We removed a lot of code, but it still works as expected!
This accurately demonstrates the point raised in the React Docs: updating
useRef
does not trigger a re-render, which is probably the most significant
difference between useRef
and useState
.
In addition, you'll notice that when we updated the current
property on our
ref
, we did so directly. Never do this with useState
. Instead, you must
always use the setter function if you wish your UI to react to the change. You
can read about this behavior
here.
Yes, I know. We literally just useduseRef
to affect a change in the DOM. In my defense, try to useuseState
to track your render count and include itself in that count. If you can figure out a way without causing an infinite re-render, please reach out. I'd like you to teach me!
Before concluding this article, I’d like to address what I think is the simplest
common use case for useRef
. Let’s start by moving our render count below the
section
tag containing our form. We’ll put it in its own section tag.
<section>
<h3>Render Count: {rendersRef.current}</h3>
</section>
Next, we will create a formRef
for our form and assign it accordingly. Let’s
add a button with the text, Scroll to Render Count
.
const formRef = useRef(null);
...
<section ref={formSectionRef}>
<form onSubmit={handleSubmit}>
...
</form>
<button>Scroll to Render Count</button>
Then we will initialize renderCountSectionRef
, assign it to our render count
container, and add a button with the text Scroll to Form
.
const renderCountSectionRef = useRef(null);
...
<section ref={renderCountSectionRef}>
<h3>Render Count: {rendersRef.current}</h3>
<button>Scroll to Form</button>
</section>
Let’s create a new function called scrollToElement
that expects a ref
as a
parameter and scrolls us to said ref
.
const scrollToElement = ref => {
ref.current.scrollIntoView({ behavior: 'smooth' });
};
Now, we will set the onClick
property to scrollToElement
with the
appropriate ref
as its argument.
Please note that using inline functions will make for a less performant app, but that refactor would require more custom logic and is outside the scope of this article.
<button
onClick={() => {
scrollToElement(renderCountSectionRef);
}}
>
Scroll to Render Count
</button>
...
<button
onClick={() => {
scrollToElement(formSectionRef);
}}
>
Scroll to Form
</button>
Now, when we click the buttons, we scroll about the page! This feels a lot like
a Scroll To Top
or Jump to Recipe
button, doesn't it?
Since scrolling to a specific element doesn't demand a re-render, it is a much
better use case for useRef
than useState
.
After realizing that we can affect changes on DOM Elements directly with
useRef
, it may be tempting to use this method as an analog to querySelector
or getElementBy—
.
This is an anti-pattern and should be avoided if possible. It can cause your UI to fall out of sync with your state and, in a larger application, can have an unforeseen ripple effect. These bugs will be challenging to track down.
In this article, we discussed the functionality of useRef
, how it persists
through renders, and how, unlike useState
, it does not cause re-renders. We
also explored one of the most common uses of useRef
: to navigate our user to
different DOM elements.
I’m always looking for new friends and colleagues. If you found this article helpful and would like to connect, you can find me at any of my homes on the web.
GitHub | Twitter | LinkedIn | Website | Medium | Dev.to