New render, same value (useRef)
The marketing pitch for **useState**
is that it allows you to add state to function components. This is true, but we can break it down even further. Fundamentally, the **useState**
Hook gives you two things - a value that will persist across renders and an API to update that value and trigger a re-render.
When building UI, both are necessary. Without the ability to persist the value across renders, you'd lose the ability to have dynamic data in your app. Without the ability to update the value and trigger a re-render, the UI would never update.
Now, what if you had a use case where you weren't dealing with any UI, so you didn't care about re-rendering, but you did need to persist a value across renders? In this scenario, it's like you need the half of **useState**
that lets you persist a value across renders but not the other half that triggers a re-render — Something like this.
Alright, stick with me here. Remember, **useState**
returns an array with the first element being a value that will persist across renders and the second element being the updater function which will trigger a re-render. Since we only care about the first element, the value, we append **[0]**
to the invocation. Now, whenever we invoke **usePersistentValue**
, what we'll get is an object with a **current**
property that will persist across renders.
If it's still fuzzy, looking at an actual example may help.
If you're not familiar with the native browser APIs setInterval and clearInterval, you can read about them here before continuing on.
Let's say we were tasked to build an app that had a counter that incremented by 1 every second and a button to stop the counter. How would you approach this? Here's what one implementation might look like.
**id**
is created inside of **useEffect**
but we need to access it inside of the **clear**
event handler to stop the interval. To do that, we move the declaration of **id**
up to the main scope and then initialize it with the **id**
when the effect runs.
All good, right? Sadly, no. The reason for this is because **id**
doesn't persist across renders. As soon as our **count**
state variable changes, React will re-render **Counter**
, re-declaring **id**
setting it back to **undefined**
.
What we need is a way to persist the **id**
across renders 😏. Luckily for us, we have our **usePersistentValue**
Hook we created earlier. Let's try it out.
Admittedly, it's a bit hacky but it gets the job done. Now instead of **id**
being re-declared on every render, because it's really a value coming from **useState**
, React will persist it across renders.
As you probably guessed by now, the ability to persist a value across renders without causing a re-render is so fundamental that React comes with a built-in Hook for it called **useRef**
. It is, quite literally, the same as our **usePersistentValue**
Hook that we created. To prove this, here's the exact same code as before except with **useRef**
instead of **usePersistentValue**
.
**useRef**
follows the same API we created earlier. It accepts an initial value as its first argument and it returns an object that has a **current**
property (which will initially be set to whatever the initial value was). From there, anything you add to **current**
will be persisted across renders.
The most popular use case for **useRef**
is getting access to DOM nodes. If you pass the value you get from **useRef**
as a **ref**
prop on any React element, React will set the **current**
property to the corresponding DOM node. This allows you to do things like grab input values or set focus.
If you want to add state to your component that persists across renders and can trigger a re-render when it's updated, go with **useState**
or **useReducer**
. If you want to add state to your component that persists across renders but doesn't trigger a re-render when it's updated, go with **useRef**
.