Web Dev Simplified Blog

Why You Should Never Store Derived State

November 4, 2019

We have all done it. You are working on a React app when all of a sudden your state becomes out of sync. There are many reasons for this problem, but incorrectly storing derived state is one of the most common and hardest to spot causes.

What Is Derived State?

So it is bad to mishandle derived state, but what exactly is derived state. In essence, derived state is some state in an application that can be derived or created from the already stored state of that application. Since this definition is pretty vague, here is a concrete example of derived state. Imagine an app with 3 counters that all store their count in state. The total of adding those 3 counters together would be derived state since that total value is derived from the counter values in the state. Storing that total in the state is what needs to be avoided, because when derived state is stored in state it makes it very easy for state to become out of sync. Let’s look at a more concrete example.

How To Spot Derived State

Imagine a component that has state for a list of users with a name and id.

function User() {
  const [users, setUsers] = useState([
    { id: 1, name: "Kyle" },
    { id: 2, name: "John" },
  ])

  function updateUser(id, name) {
    setUsers(prevUsers => {
      const newUsers = [...prevUsers]
      const user = newUsers.find(user => user.id === id)
      user.name = name
      return newUsers
    })
  }

  return users.map(user => user.name).join(", ")
}

This is all good and there is no derived state, but what if this app needed to account for the selectedUser as well. One way that many people tackle this problem is by doing this.

function User() {
  const [users, setUsers] = useState([
    { id: 1, name: 'Kyle' },
    { id: 2, name: 'John' }
  ])
  const [selectedUser, setSelectedUser] = useState()

  function selectUser(id) {
    const user = users.find(user => user.id === id)
    setSelectedUser({ ...user }
  }

  function updateUser(id, name) {
    setUsers(prevUsers => {
      const newUsers = [...prevUsers]
      const user = newUsers.find(user => user.id === id)
      user.name = name
      return newUsers
    })
  }

  return users.map(user => user.name).join(', ')
}

At first glance this code may look correct, and it most likely will work when you test it, but there is a huge problem. This code is storing derived state. At first it isn’t obvious any derived state is stored since none of the state directly adds up into any of the other state, but the selectedUser is derived state from the users array. This is because the name of the selectedUser is defined in the users array and should not be duplicated in the selectedUser variable. By having this duplication the app needs to update the selectedUser every time that user is changed in the users array. To illustrate why this is a problem here is some code that sets the selectedUser and then updates that user in the users array.

function User() {
  const [users, setUsers] = useState([
    { id: 1, name: "Kyle" },
    { id: 2, name: "John" },
  ])
  const [selectedUser, setSelectedUser] = useState()

  useEffect(() => {
    selectUser(1)
    updateUser(1, "Kate")
  }, [])

  function selectUser(id) {
    const user = users.find(user => user.id === id)
    setSelectedUser({ ...user })
  }

  function updateUser(id, name) {
    setUsers(prevUsers => {
      const newUsers = [...prevUsers]
      const user = newUsers.find(user => user.id === id)
      user.name = name
      return newUsers
    })
  }

  return users.map(user => user.name).join(", ")
}

After this component runs it will set the selectedUser to a copy of user 1 which has a name of Kyle. It will then update user 1 to give them a name of Kate. This leaves the state of the application out of sync since the selectedUser has a name of Kyle still since it was not updated, but the users array has the correct name of Kate for that user. Luckily, this is incredibly easy to fix. All the component needs to do is store the id of the selected user instead of a copy of the entire user.

function User() {
  const [users, setUsers] = useState([
    { id: 1, name: "Kyle" },
    { id: 2, name: "John" },
  ])
  const [selectedUserId, setSelectedUserId] = useState()
  const selectedUser = users.find(user => {
    return user.id === selectedUserId
  })

  useEffect(() => {
    selectUser(1)
    updateUser(1, "Kate")
  }, [])

  function selectUser(id) {
    setSelectedUserId(id)
  }

  function updateUser(id, name) {
    setUsers(prevUsers => {
      const newUsers = [...prevUsers]
      const user = newUsers.find(user => user.id === id)
      user.name = name
      return newUsers
    })
  }

  return users.map(user => user.name).join(", ")
}

Now the selectedUser is being derived from the state instead of being stored in the state. This means the component never has to worry about updating the users array without updating the selectedUser.

Bonus Tip (useMemo)

The one big downside to not storing data in state is that the data needs to be recomputed every render. Luckily, React has thought of this already and has a hook called useMemo for this exact problem. When state is derived that is slow and/or cpu intensive the useMemo hook can be used to only recompute the value when the state it is derived from is changed. In our previous example the code that sets the selectedUser would look like this if it used useMemo.

const selectedUser = useMemo(() => {
  return users.find(user => user.id === selectedUserId)
}, [users, selectedUserId])

This hook works very similar to useEffect in that the first parameter is a function that is run every time the dependencies in the second argument array change. This means that to use this hook the first parameter should be the function that derives the state just as if useMemo was not being used. Then the second parameter is an array of all the state that this variable is derived from. It is as simple as that. React will take care of all the memoization for you.

As a reminder, useMemo should only be used with values that are slow to calculate. In this example the selectedUser is quick to calculate so the extra overhead of useMemo will not give any speed increases and may actually slow the app down.