DEV Community

Cover image for Don’t mutate state in React.js — References vs Values
LTV Co. Engineering
LTV Co. Engineering

Posted on • Originally published at ltvco.com

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

In this post


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 objects 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 Boolean, null , 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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"}
  ]
})
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!


Interested in working with us? Have a look at our careers page and reach out to us if you would like to be a part of our team!


Image description

Top comments (0)