Don't over useState
Tags: javascript, react, usestate, programming
Published: 2020-12-29 15:53:35

`useState` is considered to be the most basic of all the hooks provided by React. It is also the one you are most likely to use (no pun intended), alongside `useEffect`.

Yet over the last couple of months, I have seen this hook being misused a lot. This has mostly nothing to do with the hook itself, but because state management is never easy.

This is the first part of a series I'm calling _useState pitfalls_, where I will try to outline common scenarios with the useState hook that might better be solved differently.

# What is state?

I think it all boils down to understanding what state is. Or more precisely, what state _isn't_. To comprehend this, we have to look no further than [the official react docs](https://reactjs.org/docs/thinking-in-react.html#step-3-identify-the-minimal-but-complete-representation-of-ui-state):

> Ask three questions about each piece of data:

> Is it passed in from a parent via props? If so, it probably isn’t state.
> Does it remain unchanged over time? If so, it probably isn’t state.
> Can you compute it based on any other state or props in your component? If so, it isn’t state.

So far, so easy. Putting _props to state_ (1) is a whole other topic I will probably write about another time, and if you are not using the setter at all (2), then it is hopefully pretty obvious that we are _not_ dealing with state.

That leaves the third question: _derived state_. It might seem quite apparent that a value that can be computed from a state value is _not_ it's own state. However, when I reviewed some code challenges for a client of mine lately, this is exactly the pattern I have seen a lot, even from senior candidates.

# An example

The exercise is pretty simple and goes something like this: Fetch some data from a remote endpoint (a list of items with categories) and let the user filter by the category.

The way the state was managed looked something like this most of the time:

```jsx
import { fetchData } from './api'
import { computeCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
    const [categories, setCategories] = React.useState([])

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])

    React.useEffect(() => {
        if (data) {
            setCategories(computeCategories(data))
        }
    }, [data])

    return <>...</>
}
```

At first glance, this looks okay. You might be thinking: We have an effect that fetches the data for us, and another effect that keeps the categories _in sync_ with the data. This is exactly what the useEffect hook is for (keeping things in sync), so what is bad about this approach?

# Getting out of sync

This will actually work fine, and it's also not totally unreadable or hard to reason about. The problem is that we have a "publicly" available function `setCategories` that future developers might use.

If we intended our categories to be solely dependent on our data (like we expressed with our useEffect), this is bad news:

```jsx
import { fetchData } from './api'
import { computeCategories, getMoreCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
    const [categories, setCategories] = React.useState([])

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])

    React.useEffect(() => {
        if (data) {
            setCategories(computeCategories(data))
        }
    }, [data])

    return (
        <>
            ...
            <Button onClick={() => setCategories(getMoreCategories())}>Get more</Button>
        </>
    )
}
```

Now what? We have no predictable way of telling what "categories" are.

-   The page loads, categories are _X_
-   User clicks the button, categories are _Y_
-   If the data fetching re-executes, say, because we are using [react-query](https://react-query.tanstack.com/), which has features like automatic re-fetching when you focus your tab or when you re-connect to your network (it's awesome, you should give it a try), the categories will be _X_ again.

Inadvertently, we have now introduced a hard to track bug that will only occur every now and then.

# No-useless-state

Maybe this is not so much about useState after all, but more about a misconception with useEffect: It should be used to sync your state _with something outside of React_. Utilizing useEffect to sync _two react states_ is rarely right.

So I'd like to postulate the following:

> Whenever a state setter function is only used synchronously in an effect, get rid of the state!

— TkDodo

This is loosely based on what @sophiebits posted recently on twitter:

{% twitter 1293710971274289152 %}

This is solid advice, and I'd go even further and suggest that unless we have proven that the calculation is expensive, I wouldn't even bother to memoize it. Don't prematurely optimize, always measure first. We want to have proof that something is slow before acting on it. For more on this topic, I highly recommend [this article by @ryanflorence](https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578).

In my world, the example would look just like this:

```diff
import { fetchData } from './api'
import { computeCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
-   const [categories, setCategories] = React.useState([])
+   const categories = data ? computeCategories(data) : []

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])
-
-   React.useEffect(() => {
-       if (data) {
-           setCategories(computeCategories(data))
-       }
-   }, [data])

    return <>...</>
}
```

We've reduced complexity by halving the amount of effects and we can now clearly see that categories is _derived from_ data. If the next person wants to calculate categories differently, they have to do it from _within_ the `computeCategories` function. With that, we will always have a clear picture of what categories are and where they come from.

A single source of truth.