Skip to main content

5.4 - Updating Objects and Arrays in State


State can hold any kind of JavaScript value, including objects. But you shouldn’t change objects that you hold in the React state directly–instead, to update an object, you need to create a new instance (or make a copy), and then set the state to use that copy.

warning

Although objects in React state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them.

This example holds an object in state to represent the current pointer position. The red dot is supposed to move when you touch or move the cursor over the preview area. But this code is buggy: the dot always stays in the initial position:

import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}

The problem is with this bit of code.

onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}

This code modifies the object assigned to position from the previous render. But without using the state setting function, React has no idea that object has changed. So React does not do anything in response. It’s like trying to change the order after you’ve already eaten the meal.

To actually trigger a re-render in this case, create a new object and pass it to the state setting function:

onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}

With setPosition, you’re telling React:

  1. Replace position with this new object
  2. And render this component again

Now the code should work as intended: the red dot should follow your cursor.


Copying objects with spread syntax

In the previous example, the position object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields.

These input fields don’t work because the onChange handlers mutate the state:

onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}

With setPosition, you’re telling React:

  1. Replace position with this new object
  2. And render this component again
import { useState } from 'react';

export default function Form() {
const [person, setPerson] = useState({
firstName: 'Elaine',
lastName: 'S',
email: 'elaineS@berkeley.edu'
});
// these don't work because they are mutating the state from a past render.

function handleFirstNameChange(e) {
person.firstName = e.target.value;
}

function handleLastNameChange(e) {
person.lastName = e.target.value;
}

function handleEmailChange(e) {
person.email = e.target.value;
}

return (
<>
<label>
First name:
<input
value={person.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={person.lastName}
onChange={handleLastNameChange}
/>
</label>
<label>
Email:
<input
value={person.email}
onChange={handleEmailChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}

The reliable way to get the behavior you’re looking for is to create a new object and pass it to setPerson. But here, you want to also copy the existing data into it because only one of the fields has changed:

setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});

You can use the ... object spread syntax so that you don’t need to copy every property separately.

setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});

Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly!

Recap

  1. Treat all state in React as immutable.
  2. When you store objects in state, mutating them will not trigger renders and will change the state in previous render “snapshots”.
  3. Instead of mutating an object, create a new version of it, and trigger a re-render by setting state to it.
  4. You can use the {...obj, something: 'newValue'} object spread syntax to create copies of objects.

Updating Arrays in State

Arrays are mutable in JavaScript, but you should treat them as immutable when stored in state. This means when you need to update an array in state, you must create a new instance and then set state to use the new array.

Updating Arrays without Mutation

In JavaScript, arrays are just another kind of object. Like with objects, you should treat arrays in React state as read-only. This means that you shouldn’t reassign items inside an array like arr[0] = 'bird', and you also shouldn’t use methods that mutate the array, such as push() and pop().

Instead,you can create a new array from the original array in your state by calling its non-mutating methods like filter() and map(). Then you can set your state to the resulting new array.

Here is a reference table of common array operations. When dealing with arrays inside React state, you will need to avoid the methods in the left column, and instead prefer the methods in the right column:

OperationAvoid (mutates the array)Prefer (returns a new array)
Addingpush, unshiftconcat, [...arr] spread syntax (example)
Removingpop, shift, splicefilter, slice (example)
Replacingsplice, arr[i] = ... assignmentmap (example)
Sortingreverse, sortCopy the array first (example)
Insertingsplice[...arr.slice(0, index), newItem, ...arr.slice(index)] (example)
warning

Unfortunately, slice and splice are named similarly but are very different:

slice lets you copy an array or a part of it. splice mutates the array (to insert or delete items).

In React, you will be using slice (no p!) a lot more often because you don’t want to mutate objects or arrays in state.