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(), )
const [value] = useState(() => computeValue())
And the answer is that they both work, but neither is ideal. Let’s see why.
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
useMemoas 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 without
useMemo- 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?
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?
Essentially, useRef is like a “box” that can hold a mutable value in its .current property.
useRef()is useful for more than the
refattribute. 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?
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:
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:
- for storing values that persist across renders;
- updates trigger a re-render;
- updates via setter function.
- also for storing values that persist across renders;
- updates don’t trigger a re-render;
- mutable directly via the
- for performance optimizations only
Originally published at DEV.to.