What is React to do with cutting holes on a cardboard? Surprisingly, they have a lot in common.
useRef is a built-in React Hook. Perhaps you have used it to access DOM nodes (If not, don't worry. It's covered in this post).
But do you know it can do way more? It's a hidden gem 💎. Trust me, it can become a powerful tool in your arsenal.
This is a deep dive into
useRef. With an example-driven approach, this article covers:
Along the way, we'll build a stopwatch and a like button (yes, exactly the same like button on this blog, feel free to steal the code):
useRef Hook in React can be used to directly access DOM nodes, as well as persist a mutable value across rerenders of a component.
When combined with the
ref attribute, we could use
useRef to obtain the underlying DOM nodes to perform DOM operations imperatively. In fact, this is really an escape hatch. We should only do this sparsely for things that React doesn't provide a declarative API for, such as focus management. If we use it for everything (in theory we could), we'd lose the benefit of React's declarative programming paradigm.
This is what I'm going to focus on in this article.
Keep reading 👇.
Here's the mental model that works for me:
useRefis like class instance variable for function components.
After a lot of struggle trying to understand
To understand how
useRef works and why we need it, let's get started with a contrived (but useful) example. Can you build a counter without using the
Let's give it a try:
This doesn't work, even though the value of
count increases on click. Why? React is oblivious to the change of local variables and, therefore, doesn't know when to update the DOM. We'd need to rerender the component, namely request React to call the component function
Counter, to let the change reflected on the screen.
You can verify this by uncommenting the line
console.log('render') right after
let count = 0. The component isn't rendered when the user clicks the button.
OK. Then what if we force the component to render when
This doesn't work either. The count displayed on the page stays zero. Even worse, the count value in the console stopped working too. It's always
useForceRender above is a custom hook:
Why? This is what I call rerendering time loop.
Every time when the component rerenders, it goes into a time loop. All the local variables in the function will be reset to their original values. That's why
count is always
That's no surprise, right? They are local variables defined in the function. When the function reruns, the variables are supposed to behave like that!
As straighforward as this sounds, sometimes it's easy to forget (I do it all the time). So remember, since a React component is rerendered all the time, all the local variables will go into a time loop, over and over again.
So, we need a solution that survives this "rerendering time loop". We want to preserve the value no matter how many times the component is rendered. If we update
42, at the next render it should still be
42, not the initial value
Let's try the first approach: what if we move the variable out of the component function? It will no longer be affected by the rerendering time loop, right?
That's correct! It works! Yay!
But wait a second, what if we want to have two counters?
So our counters are still broken. Did you try it in the preview window? The two counters are tied together. Clicking one button changes the other counter too!
That's because both counters are tied to the same variable
count. What we need instead is to have a separate variable for each instance of the counter.
So the requirements are:
But, but we are still using that
useForceRender to update the DOM! If we comment out
forceRender(), the counter stops working again! (try it)
What's the point of
useRef if it relies on
useState to work properly? Why don't we just use
What's the point of
useRef if it doesn't trigger a rerender when the value is updated?
In fact, being able to persist a value across rerenders without triggering another rerender is exactly why
useRef is created. Sometimes we just don't want the component to rerender when the value is updated.
It's now time to look at a better example.
Let's modify the Counter component. Now we want to display the previous value of the counter whenever the button is clicked. We can do so using a combination of
useEffect, and of course
Look, when saving the previous state in
useEffect, we definitely don't want the component to rerender. Otherwise, it'd go into an infinite loop. So,
useRef to the rescue!
Now let's look at a more useful example. How about a stopwatch?
We can make the stopwatch tick with a combination of
But how do we pause the stopwatch?
We can add another state
ticking and clear the interval when
false. Let's give it a try:
Did you see the problem? Where should we define that variable
If we define
interval in the outer scope, we'll taste the wrath of the rerendering time loop --
interval will always be
We don't want to put it in state either, since it's really not something we would want to display on the UI. It's interval stuff.
useRef to the rescue:
Note: due to the way
useRef is still the same).
Let's check out the real thing! Just a reminder, this is what we want to build (remember to click and hold):
We are going to focus on the press-and-hold behavior:
How would we implement this behavior?
In order to detect the press-and-hold gesture, we can use
setTimeout. When the mouse is down, kick off the timeout. When the mouse is up, clear the timeout.
How do we set and clear the timeout when the component is rendered and rerendered, i.e. when the state
You already know the answer from Example 2, right?
Next, how do we repeatedly increase the like count?
setInterval is what we need. And of course we'll use another ref to keep track of the interval reference across rerenders.
Finally, we can extract all the logic into a custom Hook
When using the
usePressHoldRepeat hook, remember to use
useCallback to prevent excess renders due to the fact the function might be recreated on each render (time loop).
Check out the full source code in this CodeSandbox
Remember, updating a ref's value is a side effect. We should put it inside
useEffect or event handlers. Don't update
current at the top level of a function component.
We could use
ref.current in JSX, as you've seen in Example 1. However, keep in mind that the value displayed might be stale since updating
ref.current doesn't trigger a rerender. We need to trigger rerenders in other means, e.g. with
With the understanding of how
useRef works, let's look at a few related concepts. I'm going to organize them as quiz questions since I think you'd be able to figure it out with the mental model built after reading this article this far.
Give it a try!
As mentioned, we could use the
ref attribute and
useRef to obtain the access to underlying DOM nodes, like so:
But, have you wondered how it works?
React receives the ref via the
ref attribute of
input and mutates it at some point, just as what we did in the examples above:
What is the difference between
useState? How do they relate to each other?
useState persist a value across rerenders of a component. This means the value doesn’t get reset when the component rerenders, whereas all local variables go into a time loop.
The value tracked by
useState is updated via calling the setter function, which triggers a rerender of the component. In comparison, the value tracked by
useRef is updated via direct mutation, which does not trigger a rerender.
In React, there's another function called
What is the difference between
createRef? I know I didn't cover
createRef so far in the article, but can you guess from its name?
Well. It's called createRef. So every time when it runs, it creates a new ref object.
Hmm... Does that mean
useRef does NOT create a new ref object every time when it's called?
useRef only creates a ref object for a particular component instance when it's first rendered. In the following rerenders, it'll just returns the existing ref object associated with that component instance. That's why we can trust it to persist a value across rerenders!
In function components, we should always use
creatRef is used in class components to create a ref, where we could keep it in a class instance variable (so that it's not created repeatedly when the component rerenders).
There are two use cases of
The second use is particularly powerful when combined with
useCallback. In this post, we used it in two real-world scenarios:
Do you have other examples of
useRef in your projects? Let me know!
I hope you find this article useful!
One of my 2021 goals is to write more posts that are useful, interactive and entertaining. Want to receive early previews of future posts? Sign up below. No spam, unsubscribe anytime.