DEV Community

Cover image for useRef – Demystifying React Hooks (Pt. 3)
Austin Ryan Taylor
Austin Ryan Taylor

Posted on • Originally published at austinrt.Medium

useRef – Demystifying React Hooks (Pt. 3)

In this article, we will discuss some common use cases for React's useRef Hook.

Getting Started

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 to follow along in your local IDE, you can find the GitHub Repo here.

  • fork and clone
  • cd client
  • npm i
  • npm start

Starter Code

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);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

What's the Problem?

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."

useRef

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.

useRef

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.

useRef

Minimizing Renders

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';
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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++;
});
Enter fullscreen mode Exit fullscreen mode

Let's render renders in our JSX.

<section>
  <h3>Render Count: {rendersRef.current}</h3>
  <form onSubmit={handleSubmit}>
Enter fullscreen mode Exit fullscreen mode

When we type in the input, we see our render count incrementing in real-time. Neat, I guess, but not particularly useful.

useRef

From the React Docs:

useRef returns a ref object with a single current property initially set to the initial value you provided. On the next renders, useRef will return the same object. You can change its current 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?

Refactoring Our Form with useRef

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);
Enter fullscreen mode Exit fullscreen mode

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}
  />
Enter fullscreen mode Exit fullscreen mode

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
    });
};
Enter fullscreen mode Exit fullscreen mode

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 refs did not cause the component to re-render.

useRef

With that working, we no longer need the following:

  • email onChange attribute
  • password onChange attribute
  • handleChange function
  • formValues state
  • initialFormValues object
  • useState 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!

useRef vs. useState

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 used useRef to affect a change in the DOM. In my defense, try to use useState 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!

One Last Use Case

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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' });
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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?

useRef

Since scrolling to a specific element doesn't demand a re-render, it is a much better use case for useRef than useState.

A Caution

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.

Conclusions

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

Resources

Top comments (0)