loading...

Compute values on component mount with React Hooks: State vs Ref

raicuparta profile image Ricardo Updated on ・4 min read

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(), [])
or
const [value] = useState(() => computeValue())
?

And the answer is that they both work, but neither is ideal. Let's see why.


useMemo

import computeValue from 'expensive/compute';

// ...

const value= useMemo(computeValue, []);
Enter fullscreen mode Exit fullscreen mode

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 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?


useState

import computeValue from 'expensive/compute';

// ...

const [value] = useState(computeValue)
Enter fullscreen mode Exit fullscreen mode

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 the ref 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?

import computeValue from 'expensive/compute';

// ...

const value = useRef(null)

if (value.current === null) {
  value.current = computeValue()
}
Enter fullscreen mode Exit fullscreen mode

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:

import computeValue from 'expensive/compute';

// ...

const [value, setValue] = useState(null)

useEffect(() => {
  setValue(computeValue)
}, [])
Enter fullscreen mode Exit fullscreen mode

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

Discussion

pic
Editor guide
Collapse
fnky profile image
Christian Petersen

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 of useState:

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.

Collapse
raicuparta profile image
Ricardo Author

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.

Collapse
maddes profile image
Daniel Scholtus

One extra improvement, move computevalue() definition outside of the function component to avoid re-defining the function on each cycle.

Collapse
raicuparta profile image
Ricardo Author

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!