useRef vs useState
Note: this post was updated on 10/20/2020, adding the TL;DR and editing for clarity.
TL;DR Use useRef
when you need information that is available regardless of component lifecycle and whose changes should NOT trigger rerenders. Use useState
for information whose changes should trigger rerenders.
When should you use useRef
vs useState
? The classic example to demonstrate the difference is how to keep track of whether a component is mounted when using hooks.
In a class component, a classic way to do it is to keep an isMounted
flag on the component instance, and update it in componentDidMount
and componentWillUnmount
. However, those methods aren’t available to us when using hooks.
Take this example: we have two buttons, one that increments a counter at a delay, and another that shows or hides the UI. The normal behavior is that you can click the counter a bunch of times, and when you’re done, you can hide the UI.
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setTimeout(() => {
setCount(currentCount => setCount(currentCount + 1));
}, 1500);
}
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
function Wrapper() {
const [show, setShow] = useState(true);
const toggleShow = () => {
setShow(currentShow => setShow(!currentShow));
}
return (
<div>
{show && <Counter />}
<button onClick={toggleShow}>{show ? 'Hide' : 'Show'}</button>
</div>
);
}
This example demonstrates what can happen if you try to call setState
on an unmounted component - if you click the increment button and then really quickly click the hide button, you’ll get the error Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.
(though only in development mode). We want to make sure we don’t try to set state after the component has been unmounted.
To fix this, we need to keep track of whether the component is mounted or not. Then, in our increment callback, we should be able to early return if the component isn’t mounted anymore. You might think you could write the component like this:
function Counter() {
const [count, setCount] = useState(0);
const [isMounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => {
setMounted(false);
};
});
const increment = () => {
setTimeout(() => {
if (isMounted) {
console.log('Mounted, going ahead with setting count!')
setCount(currentCount => setCount(currentCount + 1));
}
}, 1500);
};
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
However, this won’t work - even after we clearly have unmounted the component, we still get the error!
This is because our callback in setTimeout
is reading from stale state - isMounted
was true
when we wrote the function and we maintained access to that value through a closure. But, when we called setMounted(false)
, isMounted
wasn’t updated from the POV of our callback - we were still looking at the old value. Instead, we need a way to get the current value of isMounted
, even it’s changed since we invoked setTimeout
.
It turns out, that’s what ref
s are great for! From the React docs:
useRef
returns a mutable ref object whose.current
property is initialized to the passed argument (initialValue
). The returned object will persist for the full lifetime of the component.
That’s exactly what we want. So, we can rewrite our logic like this:
function Counter() {
const [count, setCount] = useState(0);
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
});
const increment = () => {
setTimeout(() => {
if (isMounted.current) {
console.log('Mounted, going ahead with setting count!')
setCount(currentCount => setCount(currentCount + 1));
}
}, 1500);
};
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
Now, if we increment a bunch and then hide the UI, we don’t log anymore!
This is because we’re using the isMounted
ref and reading from its current
value at the time we want to make a decision. And this makes sense! useState
should be used when the value of state should affect what gets rendered. useRef
should be used when you want to have a piece of information that persists “for the full lifetime of the component” - not just during its render cycle.