Don’t mutate state in React.js — References vs Values

Updated May 16, 2023
Jonathan Brierre
Don’t mutate state in React.js — References vs Values

Hello! In this blog post, I am going to go over how to properly change our React.js components’ state, and why it’s important to understand JavaScript references and values. Sometimes, our states may need to contain deeply nested object and arrays, and updating our state can sometimes be confusing and messy as a result.

When we directly mutate our state in an attempt to change it, we can often leave room for unexpected side effects and bugs in our applications. But before we talk more about updating our React state, let’s go over some of the different JavaScript data types, and breakdown references and values

Primitives

Primitive data types include Booleannull , undefined , String , and Number . In JavaScript, primitive types are passed by value. This means that when we assign a variable with any of these types, that variable will be a direct container of value for the primitive data instantiation. Let’s demonstrate why this is significant.

let x = 5
let y = x

y = y + x

console.log(y) // => 10
console.log(x) // => 5

In this example, we assign y the value of x so both x and y are initially equal to 5. Then we reassign y to the value of x + y . When we check the values of x and y with our console.log statements, we’ll see that y is now equal to 10, but x remains equal to 5. If primitives were passed by reference, then x would be equal to 10 like y . Let’s take a look at what this would look like for Objects!

Objects

Under this data type, we’ll include Array , Function , and Object . These data types are passed by** reference. **When a variable is assigned non-primitive data in JavaScript, the variable becomes a reference point to the object’s location in memory as opposed to being a direct container of value for data. Here’s a snippet to further explain the implications here:

let a = [1,2,3]
let b = a

b.push(4)
console.log(a) // => [1,2,3,4]

In this example, we assigned a an array of numbers, and we assigned b to the value of a . Because the array is a non-primitive value, both a and b point to the same array’s reference in memory. That’s why when we push a new value into b , a changes as well to reflect that push.

Nested State in React

Let’s say that we want to initialize some nested state for a functional component. An example can be like this:

const [person, setPerson] = useState({
  name: "John",
  pets: [
    {name: "Fido", animal: "dog"},
    {name: "Mr. Whiskers", animal: "cat"}
  ]
})

Let’s say we want to make edits to this person state here. Here is how we SHOULD NOT update our state:

person.pets.pop()
person.pets.push({name: "Polly", animal: "parrot"})
person.name = "Jane"

Never do we ever want to directly mutate our state like this. Not only do these changes not trigger a rerender of our component, but they can introduce bugs in the code and UI. If we are needing to transform our state data, we should always look to make copies of our Objects before making any changes to them. For example:

const personPetsCopy = [...person.pets];
personPetsCopy.pop()
personPetsCopy.push({name: "Polly", animal: "parrot"})

setPerson({
  ...person,
  pets: personPetsCopy
})

We can also use the filter and map array methods to make copies of our arrays in state. For example, if we want to delete an object in our pets array, we can use the filter method.

const personPetsCopy = person.pets.filter((pet) => pet.name !== "Fido")

setPerson({
  ...person,
  pets: personPetsCopy
})

Or if we want to edit one of the pets in our array, we could use the map function as well!

const personPetsCopy = person.pets.map((pet) => {
  if (pet.animal === 'dog') {
    return {
      ...pet,
      name: 'doggie',
      isGoodBoy: true
    }
  }
  return pet
})

When mapping to make edits, also be sure to not accidentally mutate your state! A state mutation in the above example could look like:

const personPetsCopy = person.pets.map((pet) => {
  if (pet.animal === 'dog') {
    pet.name = 'doggie'
    pet.isGoodBoy = true
    return pet
  }
  return pet
})

In this snippet, we are modifying the pet object in memory instead of creating a new object. As a result, this is a direct state mutation, and should be avoided.

To make cloning our deeply nested objects for transformation easier, we can use the cloneDeep function that the lodash library offers.

const personClone = cloneDeep(person)

We can do whatever we want to this personClone without us worrying about mutating our state. And when we’re done making the changes we need, we’ll just set the clone to state as we usually would do.

setPerson(personClone)

Conclusion

Updating nested states in react can be a pain for beginners, and not doing so properly can lead to unexpected bugs. Hopefully this article can help you write cleaner and more efficient code moving forward.

If you want to learn more about the useState hook in React, feel free to check out my blog post detailing how to use it!