Compute values on component mount with React Hooks: State vs Ref
(Source/Credits: https://dev.to/raicuparta/compute-values-on-component-mount-with-react-hooks-state-vs-ref-4epk)
I recently came across this question: I have a functional React component that computes a value whe...
I recently came across this question:
I have a functional React component that computes a value when the component is mounted. After mounting, this value is never updated. Which is the better approach:
const value = useMemo(() => computeValue(), [])
orconst [value] = useState(() => computeValue())
?
And the answer is that they both work, but neither is ideal. Let's see why.
useMemo
```js import computeValue from 'expensive/compute';
// ...
const value= useMemo(computeValue, []); ```
At a first glance, useMemo
might seem perfect for this. It only recomputes the value if the list of dependencies (second argument) changes. With an empty array as the dependency list, the value will only be computed on the first render. And it works. But here is the problem:
You may rely on
useMemo
as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works withoutuseMemo
— and then add it to optimize performance.
So we can't rely on useMemo
for making sure a value is only computed once. Even if it works fine now, we shouldn't assume the behavior will be the same moving forward.
So what can we rely on?
useState
```js import computeValue from 'expensive/compute';
// ...
const [value] = useState(computeValue) ```
This one is closer to the correct answer, and it actually kinda works. But it is semantically incorrect.
When we pass the computeValue
function as an argument to useState
, it is used for lazy initialization. The result is that the value will be computed, but only on the first render. Seems like what we're looking for.
The problem is that this will block the first render until our computeValue
function is done. The value will then never be updated again. So is this really a good use for component state? Let's think, what is the purpose of state in a React component?
We need state when we want the component to be able to update itself. Is that the case here? There is only ever one possible value during the component's lifetime, so no. We are using state for something other than its purpose.
So if not in the state, where do we store our computed value?
useRef
Before Hooks, you might think of refs as something you use to access a child component and focus an input. But useRef
is much simpler than that:
Essentially, useRef is like a “box” that can hold a mutable value in its .current property.
useRef()
is useful for more than theref
attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.Mutating the .current property doesn’t cause a re-render.
How is this useful here?
```js import computeValue from 'expensive/compute';
// ...
const value = useRef(null)
if (value.current === null) { value.current = computeValue() } ```
We initialize our ref with null
. Then, we immediately update value.current
to the computed value, but only if it hasn't been defined already.
The resulting behavior is identical to the previous example with useState
. The render is blocked while the value is being computed. After that, the value is immediately available to be used on the first render.
The difference is just in the implementation: we're not adding unnecessary state to the component. We are instead using a ref for its original purpose: keeping a value that persists between renders.
But what if we don't want to block rendering while the value is being computed?
useState
and useEffect
This solution will be more familiar to anyone who has tried React Hooks, as it is the standard way to do anything on component mount:
```js import computeValue from 'expensive/compute';
// ...
const [value, setValue] = useState(null)
useEffect(() => { setValue(computeValue) }, []) ```
useEffect
will run after the first render, whenever the dependency list changes. In this case, we set the dependency list to []
, an empty array. This will make the effect run only after the first render, and never again. It does not block any renders.
Our first render will run with value = null
, while the value is being computed. As soon as the computation is done, setValue
will be called, and a re-render is triggered with the new value in place. The effect won't run again unless the component is re-mounted.
And this time it makes sense to have state, because there are two states the component can be in: before and after computing the value. This also comes with a bonus: we can show a "Loading..." message while the value is cooking.
Conclusion: State vs Ref vs Memo
The main lesson here is the difference between these:
useState
:- for storing values that persist across renders;
- updates trigger a re-render;
- updates via setter function.
useRef
:- also for storing values that persist across renders;
- updates don't trigger a re-render;
- mutable directly via the
.current
property. useMemo
:- for performance optimization only
Comments section
maddes
•May 1, 2024
One extra improvement, move
computevalue()
definition outside of the function component to avoid re-defining the function on each cycle.raicuparta Author
•May 1, 2024
Good point, the idea was always that
computeValue()
was something imported from somewhere else but I didn't make that clear in my minimal examples. I will update it.Thanks!
fnky
•May 1, 2024
Using
setValue
, as mentioned in the article, will force the component to update, which is undesirable in this case. The solution is to pass a function to the first argument ofuseState
:``` const [value] = useState(() => computeValue());
```
This will be initialized when component is mounted and the value will be available before rendering without forcing the component to update.
raicuparta Author
•May 1, 2024
I already mentioned that solution in the article. My solution will do an extra render, yes. But that's a good thing here, because the component won't be blocked from rendering while the value is being computed.