cover-photo

What is derived state?

Derived state in React refers to state that is calculated based on other state or props, rather than being directly set or updated by the component. While it may seem convenient to use derived state in certain cases, it can lead to a range of issues, including performance problems, bugs, and unexpected behaviour. Therefore, it is generally recommended to avoid using derived state whenever possible.

I am also guilty of this myself, as it seems like a very convenient way to organise your states. For example, a selected user can be derived from a list of users. Let us see an example for this below

How to spot derived state?

Let us consider the below snippet which has a list of users and a function to update the name of a user.

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(', ')
}

Looks good till now. But what if you also need to account for a selectedUser?

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(', ')
}

You may have noticed that we included a new selectUser function and a selectedUser state that now stores a complete user object.

This may seem simple enough at the moment, but it also brings its own overheads.

One significant issue is that selectedUser is now a derived state from the users array. In other words, the name of the user in selectedUser is derived from the name of the user in the users array.

Now let's simulate the flow of a user selecting "Kyle" from the list and changing the name to "Kate" with the following code snippet:

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(', ')
}

As you can see, the selectedUser state has now been left out of sync and you need to write additional code to keep these two object in sync.

This is the fundamental problem with derived state.

Now let us see an alternative approach to solve this.

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(', ')
}

We removed the selectedUser state and now only maintain a selectedUserId instead. For the name, we added a simple JavaScript object that uses the selectedUserId to find the selected user object.

UseMemo() much?

One potential downside to the previous solution is that the seletedUser JavaScript object needs to be recomputed every time the component re-renders. While this may not be much of an issue for this simple example, as applications scale, it will end up adding a lot of computational overhead on each render.

UseMemo() to the rescue

I feel like useMemo was built for something exactly like this.

Instead of computing something on each re-render, useMemo provides a way to return values only when some things change. We can see what that looks like in the below snippet

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

This hooks functions similar to the useEffect hook in that the the first parameter is a function that is run each time the dependencies in the second array are modified.

However, it is worth noting that including useMemo adds some overhead to the component and should not be used for very simple calculations.