React useRef Hook By Example: A Complete Guide (2024)

Last Updated:

table of contents

  • What is useRef used for?
  • Directly access DOM nodes
  • Persist a mutable value across rerenders
  • The mental model of useRef
  • The "rerendering time loop"
  • Component instances
  • Example 1: previous value / state
  • Example 2: stopwatch (clear interval)
  • Example 3: Press and hold to repeat
  • Anti-patterns
  • Assign to ref.current in render
  • Use ref.current in JSX and expect it to be reactive
  • ref and useRef
  • useRef vs. useState
  • useRef vs. createRef
  • Conclusion
Discuss on Twitter

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:

  • What it is used for
  • Its mental model
  • Simple examples
  • More practical examples
  • Anti-patterns
  • Related concepts
    • ref and useRef
    • useRef vs. useState
    • useRef vs. createRef

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):

00:00.00

Try me

Click and hold

Intended audience

This article assumes that you are familiar with React basics, as well as how to use the useState and useEffect hooks.

What is useRef used for?

The useRef Hook in React can be used to directly access DOM nodes, as well as persist a mutable value across rerenders of a component.

Directly access DOM nodes

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.

Persist a mutable value across rerenders

This is what I'm going to focus on in this article.

Keep reading πŸ‘‡.

The mental model of useRef

Here's the mental model that works for me:

useRef is like class instance variable for function components.

After a lot of struggle trying to understand useRef, I finally saw this single sentence. And it clicked! If you are hearing the same clicking sound in your mind right now already, feel free to skip to the examples section. Otherwise, if you are not familiar with JavaScript classes or class instance variables, read on. This section is for you.

The "rerendering time loop"

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 useState hook?

Let's give it a try:

JSX (Editable)

function Counter() {

let count = 0

// console.log('render')

return (

<div className="flex flex-col justify-center items-center m-8">

<div>{count}</div>

<button

className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => {

console.log(count)

count++

}}

>

+1

</button>

</div>

)

}

Preview

Console

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 count updates?

JSX (Editable)

function Counter() {

const forceRender = useForceRender()

let count = 0

return (

<div className="flex flex-col justify-center items-center m-8">

<div>{count}</div>

<button

className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => {

console.log(count)

count++

forceRender()

}}

>

+1

</button>

</div>

)

}

Preview

Console

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 0!

BTW, useForceRender above is a custom hook:

JSX

// I know, I broke my own rule of not using useState, but trust me, it's all for your good. :)

function useForceRender() {

const setC = React.useState(0)[1]

return () => setC((c) => c + 1)

}

Why? This is what I call rerendering time loop.

React useRef Hook By Example: A Complete Guide (3)

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

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.

Does this "rerendering time loop" metaphor make sense to you?

πŸ‘

πŸ‘Ž

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 count to 42, at the next render it should still be 42, not the initial value 0.

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?

JSX (Editable)

let count = 0

function Counter() {

const forceRender = useForceRender()

return (

<div className="flex flex-col justify-center items-center m-8">

<div>{count}</div>

<button

className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => {

count++

forceRender()

}}

>

+1

</button>

</div>

)

}

Preview

That's correct! It works! Yay!

Component instances

But wait a second, what if we want to have two counters?

JSX (Editable)

function TwoCounters() {

return (

<div>

Click both buttons a few times:

<Counter />

<Counter />

</div>

)

}

//

let count = 0;

function Counter() {

const forceRender = useForceRender()

return (

<div className="flex flex-col justify-center items-center m-8">

<div>{count}</div>

<button

className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => {

count++

forceRender()

}}

>

+1

</button>

</div>

)

}

Preview

Click both buttons a few times:

Console

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:

  1. Persist a value across rerenders
  2. Persist the value per component instance, throughout the full lifetime of that instance

Introducing useRef!

JSX (Editable)

function TwoCounters() {

return (

<div>

<Counter />

<Counter />

</div>

)

}

//

function Counter() {

const forceRender = useForceRender()

/* 1. Get a "variable" that corresponds to the CURRENT INSTANCE of the component */

let count = React.useRef(0)

return (

<div className="flex flex-col justify-center items-center m-8">

{/* 2. Use the value stored inside the "variable" */}

<div>{count.current}</div>

<button

className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => {

/* 3. Update the value of the "variable" */

count.current = count.current + 1

forceRender()

}}

>

+1

</button>

</div>

)

}

Preview

Console

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 useState?

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.

Example 1: previous value / state

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 useState, useEffect, and of course useRef.

JSX (Editable)

function Counter() {

const [count, setCount] = useState(0)

const prevCount = useRef(0)

useEffect(() => {

// This runs every time AFTER Counter is rendered.

prevCount.current = count

})

return (

<div className="flex flex-col justify-center items-center m-8">

<div>

Prev: {prevCount.current}, Count: {count}

</div>

<button className="bg-purple-700 px-6 text-white rounded-md hover:bg-purple-500"

onClick={() => setCount((c) => c + 1)}>+1</button>

</div>

)

}

Preview

Prev:

, Count:

Console

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!

Example 2: stopwatch (clear interval)

Now let's look at a more useful example. How about a stopwatch?

00:00.00

Try me

We can make the stopwatch tick with a combination of useState, useEffect and setInterval:

JSX

function StopWatch() {

const [milliSeconds, setMilliSeconds] = useState(0);

useEffect(() => {

const interval = setInterval(() => {

setMilliSeconds((ms) => ms + 1)

}, 1);

return () => clearInterval(interval);

}, []);

return ...

}

But how do we pause the stopwatch?

We can add another state ticking and clear the interval when ticking is false. Let's give it a try:

JSX

function StopWatch() {

const [milliSeconds, setMilliSeconds] = useState(0);

const [ticking, setTicking] = useState(false);

useEffect(() => {

if (ticking) {

const interval = setInterval(() => {

setMilliSeconds((ms) => ms + 1)

}, 1);

return () => clearInterval(interval);

} else {

// πŸ€” "interval" isn't defined here

clearInterval(interval)

}

// Remember to put "ticking" in the deps

}, [ticking]);

return ...

}

Did you see the problem? Where should we define that variable interval?

If we define interval in the outer scope, we'll taste the wrath of the rerendering time loop -- interval will always be undefined.

JSX

function StopWatch() {

const [milliSeconds, setMilliSeconds] = useState(0);

const [ticking, setTicking] = useState(false);

let interval // ❌ interval will always be undefined here

useEffect(() => {

if (ticking) {

interval = setInterval(() => {

setMilliSeconds((ms) => ms + 1)

}, 1);

return () => clearInterval(interval);

} else {

clearInterval(interval)

}

}, []);

return ...

}

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.

Again, useRef to the rescue:

JSX

function StopWatch() {

const [milliSeconds, setMilliSeconds] = useState(0)

const [ticking, setTicking] = useState(false)

// 1. Get the ref for the CURRENT INSTANCE of the component

const interval = useRef()

useEffect(() => {

if (ticking) {

// 2. Update the value via ".current"

interval.current = setInterval(() => {

setMilliSeconds((ms) => ms + 1)

}, 1)

return () => clearInterval(interval.current)

} else {

// 3. Access the value via ".current"

interval.current && clearInterval(interval.current)

}

}, [ticking])

return (

<div className="App">

<h1>{format(milliSeconds)}</h1>

<button onClick={() => setTicking((c) => !c)}>

{milliSeconds === 0 ? 'Start' : ticking ? 'Pause' : 'Resume'}

</button>

</div>

)

}

Note: due to the way setInterval works in JavaScript, the stopwatch created with the code above is VERY inaccurate. You can check out a more accurate version in CodeSandbox here (but the usage of useRef is still the same).

Example 3: Press and hold to repeat

Let's check out the real thing! Just a reminder, this is what we want to build (remember to click and hold):

Click and hold

We are going to focus on the press-and-hold behavior:

  1. When the mouse is down, wait a little bit;
  2. If the mouse is still down after a threshold, say 500 milliseconds, start repeatedly increasing the like count at an interval until the mouse is up;

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 mouseDown changes?

You already know the answer from Example 2, right? useRef!

JSX

function LikeButton(params) {

const [mouseDown, setMouseDown] = useState(false)

// 1. Create the ref that track the timeout

const holdDetectionTimeoutRef = useRef()

useEffect(() => {

if (mouseDown) {

// 2. Set the value of the ref

holdDetectionTimeoutRef.current = setTimeout(() => {

// TODO start increasing like count

}, 500);

} else {

// 3. On a new render, clear the timeout with

holdDetectionTimeoutRef.current &&

clearTimeout(holdDetectionTimeoutRef.current);

}

}, [mouseDown])

// set mouseDown in onMouseDown and onMouseUp

return ...

}

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.

JSX

function LikeButton(params) {

const [mouseDown, setMouseDown] = useState(false)

const [likes, setLikes] = useState(0)

const holdDetectionTimeoutRef = useRef()

const intervalRef = useRef(0);

useEffect(() => {

if (mouseDown) {

holdDetectionTimeoutRef.current = setTimeout(() => {

// start increasing like count

intervalRef.current = setInterval(() => {

setLikes(l => l + 1)

}, 100);

}, 500);

// Don't forget to clear both the timeout and interval

// in the clean up function of "useEffect".

return () => {

holdDetectionTimeoutRef.current && clearTimeout(holdDetectionTimeoutRef.current)

intervalRef.current && clearInterval(intervalRef.current)

}

} else {

holdDetectionTimeoutRef.current &&

clearTimeout(holdDetectionTimeoutRef.current);

intervalRef.current && clearInterval(intervalRef.current);

}

}, [mouseDown])

return ...

}

Finally, we can extract all the logic into a custom Hook usePressHoldRepeat:

JSX

function usePressHoldRepeat(

callback,

holdDetectionThreshold = 300,

repeatDelay = 100,

) {

const [mouseDown, setMouseDown] = useState(false)

const intervalRef = useRef(0)

const holdDetectionTimeoutRef = useRef(0)

const runCallback = useCallback(

() => typeof callback === 'function' && callback(),

[callback],

)

useEffect(() => {

if (mouseDown) {

holdDetectionTimeoutRef.current = setTimeout(() => {

intervalRef.current = setInterval(() => {

runCallback()

}, repeatDelay)

}, holdDetectionThreshold)

return () => {

intervalRef.current && clearInterval(intervalRef.current)

holdDetectionTimeoutRef.current &&

clearTimeout(holdDetectionTimeoutRef.current)

}

} else {

holdDetectionTimeoutRef.current &&

clearTimeout(holdDetectionTimeoutRef.current)

intervalRef.current && clearInterval(intervalRef.current)

}

}, [mouseDown, repeatDelay, runCallback, holdDetectionThreshold])

return {

onClick: runCallback,

onMouseDown: (e) => {

if (e.button === 0) setMouseDown(true)

},

onMouseUp: () => setMouseDown(false),

onMouseOut: () => setMouseDown(false),

}

}

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

JSX

function LikeButton() {

const [likes, setLikes] = useState(0);

const addLike = useCallback(() => setLikes((c) => c + 1), []);

const props = usePressHoldRepeat(addLike);

return ...

Check out the full source code in this CodeSandbox

Note: The animation in this example is implemented with Framer Motion. If you want to learn how to make the best of it and avoid common pitfalls, check out this full course I made for you!

Anti-patterns

Assign to ref.current in render

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.

JSX

function App() {

let someRef = useRef()

someRef.current = '❌ Updating a ref is a side effect!'

useEffect(() => {

someRef.current = "βœ… This is OK"

})

const handleClick = () => {

someRef.current = "βœ… This is OK too"

}

return ...

}

Use ref.current in JSX and expect it to be reactive

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

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!

ref and useRef

As mentioned, we could use the ref attribute and useRef to obtain the access to underlying DOM nodes, like so:

JSX

function Editor() {

/* 1. Call useRef */

const textboxRef = useRef()

return (

<div>

{/* 2. "Bind" the ref with the input */}

<input type="text" ref={textboxRef} />

<button

onClick={() => {

/* 3. Access the DOM node via ref.current */

textboxRef.current.focus()

}}

>

Focus

</button>

</div>

)

}

But, have you wondered how it works?

Answer

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:

JSX

// Somewhere in React code

ref.current = domNode

useRef vs. useState

What is the difference between useRef and useState? How do they relate to each other?

Answer

Both useRef and 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.

useRef vs. createRef

In React, there's another function called createRef:

JSX

const ref = React.createRef()

What is the difference between useRef and createRef? I know I didn't cover createRef so far in the article, but can you guess from its name?

Answer

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?

Exactly! 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 useRef. 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).

Conclusion

There are two use cases of useRef:

  1. Directly access DOM nodes to perform DOM operations imperatively
  2. Persist a value across rerenders, throughout the full lifetime of a component instance

The second use is particularly powerful when combined with useEffect, useState and useCallback. In this post, we used it in two real-world scenarios:

  1. Preserve previous state
  2. Preserve the reference of a timeout or interval so that we can clear them at a later time (in another render round)

Do you have other examples of useRef in your projects? Let me know!

Visits: 0

Discuss on Twitter

React useRef Hook By Example: A Complete Guide (4)

React useRef Hook By Example: A Complete Guide (5)

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.

You may also like

React useRef Hook By Example: A Complete Guide (6)

React useRef Hook By Example: A Complete Guide (7)

React useRef Hook By Example: A Complete Guide (8)

React useRef Hook By Example: A Complete Guide (9)

React Mental Models: Cutting Holes In HTML

Updated:

react

What is React to do with cutting holes on a cardboard? Surprisingly, they have a lot in common.

Read more

πŸ’ͺπŸΌπŸ‘Š

React Mental Models: Working With Input

Updated:

react

How do we work with the HTML input in React? Its behavior might be surprising. Read this post to get a good understanding of Input. And you'll get to do some Kung Fu routines with Panda too.

Read more

React useRef Hook By Example: A Complete Guide (10)

React useRef Hook By Example: A Complete Guide (11)

React Mental Models: The True Face of JSX

Updated:

react

What is an JSX tag really? It's JavaScript code in disguise. Let's reveal its true face!

Read more

React useRef Hook By Example: A Complete Guide (2024)

References

Top Articles
Latest Posts
Article information

Author: Prof. Nancy Dach

Last Updated:

Views: 5956

Rating: 4.7 / 5 (77 voted)

Reviews: 84% of readers found this page helpful

Author information

Name: Prof. Nancy Dach

Birthday: 1993-08-23

Address: 569 Waelchi Ports, South Blainebury, LA 11589

Phone: +9958996486049

Job: Sales Manager

Hobby: Web surfing, Scuba diving, Mountaineering, Writing, Sailing, Dance, Blacksmithing

Introduction: My name is Prof. Nancy Dach, I am a lively, joyous, courageous, lovely, tender, charming, open person who loves writing and wants to share my knowledge and understanding with you.