Compute values on component mount with React Hooks: State vs Ref
Tags: react, javascript, webdev
Published: 2019-08-27 02:07:25

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.

<hr>

## `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**.](https://reactjs.org/docs/hooks-reference.html#usememo) 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?

<hr>

## `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](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state). 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?

<hr>

## `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](https://reactjs.org/docs/hooks-reference.html#useref):
> 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?

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

<hr>

## `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.

<hr>

## 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
<hr>