React is built around reactivity. You make a change or update a value, and the UI updates instantly. Almost every functional component in React has some state. Today we will discuss how we can use the React useRef hook correctly and explore different use cases for useRef.
Whenever you create a new function, you will most likely assign some state to it so that your UI would re-render whenever your data changes.
const [counter, setCounter] = useState(0);
Later if you want to update the counter:
setCounter((counter) => counter + 1);
And then you are representing your state somewhere in your UI.
return (
<div className="App">
<p>{counter}</p>
</div>
);
But let's say that you wanted to store a value and update it without re-rendering your component. The simplest option would be to define a variable in top level scope of your component.
Define a variable in the component scope
Suppose that you wanted to store the previous value of counter
in a prevCount
variable whenever you updated counter
. Normally, you'd go about it like this:
function Counter(props) {
let prevCount = 0;
const [counter, setCounter] = useState(0);
// ...
}
And when you are ready to increment your counter
, you'll store the previous value like this:
return (
<div>
<p>{counter}</p>
<button
onClick={() => {
setCounter((counter) => {
prevCount = counter; // store previous value in prevCount
return counter + 1; // increment
});
}}
>
Increment counter
</button>
</div>
);
Looks good, no? And this should work but see what happens to the prevCount
across component renders.
Let's use a useEffect hook to print the value of prevCount
to the console whenever we update the counter
state:
useEffect(() => {
console.log('counter:', counter, 'prevCount:', prevCount);
}, [counter]);
And after building and a running:
Wait a second! Why is prevCount
always 0?!
React rendering for dummies
A React functional component is essentially a Javascript function that returns a React component. This means that all code within it is run every time the component is rendered. This sounds inefficient but React is clever enough to persist its own state across component renders but for that to happen you, the developer, have to use things like useState
.
In comparison, local variables inside a React functional component are not the responsibility of React since they are not a part of React component state. This means that whenever a component renders, like in our example when we increment the counter
, all the local variables will be reset.
At this point you might be wondering, "Well, why don't we just use useState
then?" The reason is that useState
is the wrong tool for the job if the value you want to persist across renders doesn't affect it in any way. You'll be causing unnecessary re-renders every time you update the value.
So how can you make sure your local variables are persisted across component renders without triggering any React re-renders?
Define a variable outside component scope
AKA define your variable in the global scope. I know, I know, this is a bad practice for good reasons but let's consider it for the sake of completeness.
Javascript global scope is a double edged sword. Anything you define in the global scope is accessible anywhere else throughout the lifetime of the program. That means you can define a global constant in one place and update it from wherever you want. This also means that the garbage collector cannot cleanup the memory taken by the stuff in the global scope.
The only change you need to make is shift the prevCount
variable to the global scope:
let prevCount = 0; // define prevCount in the global scope.
const Counter = () => {
....
Alright, that's simple but does it work?
Yep. It's working but there are a few problems with this approach:
- If the component unmounts, the value of
prevCount
will not reset to 0. You will manually need to reset the value every time --- and there is no simple way to do that. - If you reuse your
Counter
in multiple places, all the instances will reference the sameprevCount
value. If you haven't dropped this idea yet, this should be enough but for dummies, this means that whenever you update thecounter
state in one instance, it'll update theprevCount
variable for ALL components.
On the surface, this approach solves our problem but if you look deeper we have ended up with more problems than before. Is there a way to do all this without all this fuss?
React useRef
hook
From the React docs:
useRef
returns a mutable ref object whosecurrent
property is initialized to the passed argument (initialValue
). The returned object will persist for the full lifetime of the component.
In English, this means you can assign any value to current
property of useRef
hook and update it without causing a re-render.
Reusing our Counter example, let's try to store the prevCount
in a useRef hook:
const prevCount = useRef(0);
You'll also have to update your increment logic & your useEffect
hook. The final code would look like this:
const Counter = () => {
const prevCount = useRef(0);
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log('counter:', counter, 'prevCount:', prevCount.current);
}, [prevCount, counter]);
return (
<div className="App">
<p>{counter}</p>
<button
onClick={() => {
setCounter((counter) => {
prevCount.current = counter; // store previous value in prevCount
return counter + 1; // increment
});
}}
>
Increment counter
</button>
</div>
);
};
Does it work?
Hell yeah it works! And you get multiple benefits:
- Our
prevCount
value is persisted across re-renders. - We can update it easily by setting new value to
.current
. - The value will automatically reset when our component unmounts.
- We can reuse our
Counter
component without any issue since each component will have it's outprevCount
ref.
Common use cases of React useRef
hook
Obviously, the above example is a little too simple. In real world use case, you'd probably not use useRef like that. So how would you use it?
Accessing DOM elements directly
React was specifically made to avoid this. Direct DOM access is a bad idea for many reasons. There are very few cases where you can't use React state to update a value or a style. Despite that, React useRef
can be useful --- especially if you want to use the DOM functions of an element like focus
, blur
etc.
Suppose you wanted to focus an input
element when the counter reaches 10. You could easily do that with useRef
like this:
const Counter = () => {
const inputRef = useRef();
const [counter, setCounter] = useState(0);
useEffect(() => {
if (counter === 10 && inputRef.current) {
inputRef.current.focus();
}
}, [counter]);
return (
<div className="App">
<p>{counter}</p>
<button
onClick={() => {
setCounter((counter) => {
return counter + 1; // increment
});
}}
>
Increment counter
</button>
<br />
<br />
<input ref={inputRef} />
</div>
);
};
Conclusion
React useRef
hook is much more "useful" than you might think at first. One thing to note here is that it is just a Javascript object --- it can store anything that you need to update, and keep track of, without causing a re-render.
However, this doesn't mean you ditch useState
for useRef
. Each has its uses. React is not a slow web component framework and it is carefully built to account for many performance issues. Although useState seems like a huge step back from "just update the value", it keeps things very predictable --- single definition, single way to update it, single way to keep track of updates.
On the other hand, useRef
is useful when you need to access functions of a component or "secretly" change the style of a component without a re-render.
In the next articles, I'll discuss the most common ways React useRef
hook is misused. Till then, have a good day!
References
- Hooks API reference React Docs