Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array.
You will learn
- How to add, remove, or change items in an array in React state
- How to update an object inside of an array
- How to make array copying less repetitive with Immer
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 likearr[0] = 'bird', and you also shouldn’t use methods that mutate the array, such aspush() andpop().
Instead, every time you want to update an array, you’ll want to pass anew array to your state setting function. To do that, you can create a new array from the original array in your state by calling its non-mutating methods likefilter() andmap(). 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:
| avoid (mutates the array) | prefer (returns a new array) | |
|---|---|---|
| adding | push,unshift | concat,[...arr] spread syntax (example) |
| removing | pop,shift,splice | filter,slice (example) |
| replacing | splice,arr[i] = ... assignment | map (example) |
| sorting | reverse,sort | copy the array first (example) |
Alternatively, you canuse Immer which lets you use methods from both columns.
Pitfall
Unfortunately,slice andsplice are named similarly but are very different:
slicelets you copy an array or a part of it.splicemutates the array (to insert or delete items).
In React, you will be usingslice (nop!) a lot more often because you don’t want to mutate objects or arrays in state.Updating Objects explains what mutation is and why it’s not recommended for state.
Adding to an array
push() will mutate an array, which you don’t want:
import{useState}from'react';letnextId =0;exportdefaultfunctionList(){const[name,setName] =useState('');const[artists,setArtists] =useState([]);return(<><h1>Inspiring sculptors:</h1><inputvalue={name}onChange={e=>setName(e.target.value)}/><buttononClick={()=>{artists.push({id:nextId++,name:name,});}}>Add</button><ul>{artists.map(artist=>(<likey={artist.id}>{artist.name}</li>))}</ul></>);}
Instead, create anew array which contains the existing itemsand a new item at the end. There are multiple ways to do this, but the easiest one is to use the...array spread syntax:
setArtists(// Replace the state
[// with a new array
...artists,// that contains all the old items
{id:nextId++,name:name}// and one new item at the end
]
);Now it works correctly:
import{useState}from'react';letnextId =0;exportdefaultfunctionList(){const[name,setName] =useState('');const[artists,setArtists] =useState([]);return(<><h1>Inspiring sculptors:</h1><inputvalue={name}onChange={e=>setName(e.target.value)}/><buttononClick={()=>{setArtists([...artists,{id:nextId++,name:name}]);}}>Add</button><ul>{artists.map(artist=>(<likey={artist.id}>{artist.name}</li>))}</ul></>);}
The array spread syntax also lets you prepend an item by placing itbefore the original...artists:
setArtists([
{id:nextId++,name:name},
...artists// Put old items at the end
]);In this way, spread can do the job of bothpush() by adding to the end of an array andunshift() by adding to the beginning of an array. Try it in the sandbox above!
Removing from an array
The easiest way to remove an item from an array is tofilter it out. In other words, you will produce a new array that will not contain that item. To do this, use thefilter method, for example:
import{useState}from'react';letinitialArtists =[{id:0,name:'Marta Colvin Andrade'},{id:1,name:'Lamidi Olonade Fakeye'},{id:2,name:'Louise Nevelson'},];exportdefaultfunctionList(){const[artists,setArtists] =useState(initialArtists);return(<><h1>Inspiring sculptors:</h1><ul>{artists.map(artist=>(<likey={artist.id}>{artist.name}{' '}<buttononClick={()=>{setArtists(artists.filter(a=>a.id !==artist.id));}}> Delete</button></li>))}</ul></>);}
Click the “Delete” button a few times, and look at its click handler.
setArtists(
artists.filter(a=>a.id !==artist.id)
);Here,artists.filter(a => a.id !== artist.id) means “create an array that consists of thoseartists whose IDs are different fromartist.id”. In other words, each artist’s “Delete” button will filterthat artist out of the array, and then request a re-render with the resulting array. Note thatfilter does not modify the original array.
Transforming an array
If you want to change some or all items of the array, you can usemap() to create anew array. The function you will pass tomap can decide what to do with each item, based on its data or its index (or both).
In this example, an array holds coordinates of two circles and a square. When you press the button, it moves only the circles down by 50 pixels. It does this by producing a new array of data usingmap():
import{useState}from'react';letinitialShapes =[{id:0,type:'circle',x:50,y:100},{id:1,type:'square',x:150,y:100},{id:2,type:'circle',x:250,y:100},];exportdefaultfunctionShapeEditor(){const[shapes,setShapes] =useState(initialShapes);functionhandleClick(){constnextShapes =shapes.map(shape=>{if(shape.type ==='square'){// No changereturnshape;}else{// Return a new circle 50px belowreturn{...shape,y:shape.y +50,};}});// Re-render with the new arraysetShapes(nextShapes);}return(<><buttononClick={handleClick}> Move circles down!</button>{shapes.map(shape=>(<divkey={shape.id}style={{background:'purple',position:'absolute',left:shape.x,top:shape.y,borderRadius:shape.type ==='circle' ?'50%' :'',width:20,height:20,}}/>))}</>);}
Replacing items in an array
It is particularly common to want to replace one or more items in an array. Assignments likearr[0] = 'bird' are mutating the original array, so instead you’ll want to usemap for this as well.
To replace an item, create a new array withmap. Inside yourmap call, you will receive the item index as the second argument. Use it to decide whether to return the original item (the first argument) or something else:
import{useState}from'react';letinitialCounters =[0,0,0];exportdefaultfunctionCounterList(){const[counters,setCounters] =useState(initialCounters);functionhandleIncrementClick(index){constnextCounters =counters.map((c,i)=>{if(i ===index){// Increment the clicked counterreturnc +1;}else{// The rest haven't changedreturnc;}});setCounters(nextCounters);}return(<ul>{counters.map((counter,i)=>(<likey={i}>{counter}<buttononClick={()=>{handleIncrementClick(i);}}>+1</button></li>))}</ul>);}
Inserting into an array
Sometimes, you may want to insert an item at a particular position that’s neither at the beginning nor at the end. To do this, you can use the... array spread syntax together with theslice() method. Theslice() method lets you cut a “slice” of the array. To insert an item, you will create an array that spreads the slicebefore the insertion point, then the new item, and then the rest of the original array.
In this example, the Insert button always inserts at the index1:
import{useState}from'react';letnextId =3;constinitialArtists =[{id:0,name:'Marta Colvin Andrade'},{id:1,name:'Lamidi Olonade Fakeye'},{id:2,name:'Louise Nevelson'},];exportdefaultfunctionList(){const[name,setName] =useState('');const[artists,setArtists] =useState(initialArtists);functionhandleClick(){constinsertAt =1;// Could be any indexconstnextArtists =[// Items before the insertion point:...artists.slice(0,insertAt),// New item:{id:nextId++,name:name},// Items after the insertion point:...artists.slice(insertAt)];setArtists(nextArtists);setName('');}return(<><h1>Inspiring sculptors:</h1><inputvalue={name}onChange={e=>setName(e.target.value)}/><buttononClick={handleClick}> Insert</button><ul>{artists.map(artist=>(<likey={artist.id}>{artist.name}</li>))}</ul></>);}
Making other changes to an array
There are some things you can’t do with the spread syntax and non-mutating methods likemap() andfilter() alone. For example, you may want to reverse or sort an array. The JavaScriptreverse() andsort() methods are mutating the original array, so you can’t use them directly.
However, you can copy the array first, and then make changes to it.
For example:
import{useState}from'react';constinitialList =[{id:0,title:'Big Bellies'},{id:1,title:'Lunar Landscape'},{id:2,title:'Terracotta Army'},];exportdefaultfunctionList(){const[list,setList] =useState(initialList);functionhandleClick(){constnextList =[...list];nextList.reverse();setList(nextList);}return(<><buttononClick={handleClick}> Reverse</button><ul>{list.map(artwork=>(<likey={artwork.id}>{artwork.title}</li>))}</ul></>);}
Here, you use the[...list] spread syntax to create a copy of the original array first. Now that you have a copy, you can use mutating methods likenextList.reverse() ornextList.sort(), or even assign individual items withnextList[0] = "something".
However,even if you copy an array, you can’t mutate existing itemsinside of it directly. This is because copying is shallow—the new array will contain the same items as the original one. So if you modify an object inside the copied array, you are mutating the existing state. For example, code like this is a problem.
constnextList =[...list];
nextList[0].seen =true;// Problem: mutates list[0]
setList(nextList);AlthoughnextList andlist are two different arrays,nextList[0] andlist[0] point to the same object. So by changingnextList[0].seen, you are also changinglist[0].seen. This is a state mutation, which you should avoid! You can solve this issue in a similar way toupdating nested JavaScript objects—by copying individual items you want to change instead of mutating them. Here’s how.
Updating objects inside arrays
Objects are notreally located “inside” arrays. They might appear to be “inside” in code, but each object in an array is a separate value, to which the array “points”. This is why you need to be careful when changing nested fields likelist[0]. Another person’s artwork list may point to the same element of the array!
When updating nested state, you need to create copies from the point where you want to update, and all the way up to the top level. Let’s see how this works.
In this example, two separate artwork lists have the same initial state. They are supposed to be isolated, but because of a mutation, their state is accidentally shared, and checking a box in one list affects the other list:
import{useState}from'react';letnextId =3;constinitialList =[{id:0,title:'Big Bellies',seen:false},{id:1,title:'Lunar Landscape',seen:false},{id:2,title:'Terracotta Army',seen:true},];exportdefaultfunctionBucketList(){const[myList,setMyList] =useState(initialList);const[yourList,setYourList] =useState(initialList);functionhandleToggleMyList(artworkId,nextSeen){constmyNextList =[...myList];constartwork =myNextList.find(a=>a.id ===artworkId);artwork.seen =nextSeen;setMyList(myNextList);}functionhandleToggleYourList(artworkId,nextSeen){constyourNextList =[...yourList];constartwork =yourNextList.find(a=>a.id ===artworkId);artwork.seen =nextSeen;setYourList(yourNextList);}return(<><h1>Art Bucket List</h1><h2>My list of art to see:</h2><ItemListartworks={myList}onToggle={handleToggleMyList}/><h2>Your list of art to see:</h2><ItemListartworks={yourList}onToggle={handleToggleYourList}/></>);}functionItemList({artworks,onToggle}){return(<ul>{artworks.map(artwork=>(<likey={artwork.id}><label><inputtype="checkbox"checked={artwork.seen}onChange={e=>{onToggle(artwork.id,e.target.checked);}}/>{artwork.title}</label></li>))}</ul>);}
The problem is in code like this:
constmyNextList =[...myList];
constartwork =myNextList.find(a=>a.id ===artworkId);
artwork.seen =nextSeen;// Problem: mutates an existing item
setMyList(myNextList);Although themyNextList array itself is new, theitems themselves are the same as in the originalmyList array. So changingartwork.seen changes theoriginal artwork item. That artwork item is also inyourList, which causes the bug. Bugs like this can be difficult to think about, but thankfully they disappear if you avoid mutating state.
You can usemap to substitute an old item with its updated version without mutation.
setMyList(myList.map(artwork=>{
if(artwork.id ===artworkId){
// Create a *new* object with changes
return{...artwork,seen:nextSeen};
}else{
// No changes
returnartwork;
}
}));Here,... is the object spread syntax used tocreate a copy of an object.
With this approach, none of the existing state items are being mutated, and the bug is fixed:
import{useState}from'react';letnextId =3;constinitialList =[{id:0,title:'Big Bellies',seen:false},{id:1,title:'Lunar Landscape',seen:false},{id:2,title:'Terracotta Army',seen:true},];exportdefaultfunctionBucketList(){const[myList,setMyList] =useState(initialList);const[yourList,setYourList] =useState(initialList);functionhandleToggleMyList(artworkId,nextSeen){setMyList(myList.map(artwork=>{if(artwork.id ===artworkId){// Create a *new* object with changesreturn{...artwork,seen:nextSeen};}else{// No changesreturnartwork;}}));}functionhandleToggleYourList(artworkId,nextSeen){setYourList(yourList.map(artwork=>{if(artwork.id ===artworkId){// Create a *new* object with changesreturn{...artwork,seen:nextSeen};}else{// No changesreturnartwork;}}));}return(<><h1>Art Bucket List</h1><h2>My list of art to see:</h2><ItemListartworks={myList}onToggle={handleToggleMyList}/><h2>Your list of art to see:</h2><ItemListartworks={yourList}onToggle={handleToggleYourList}/></>);}functionItemList({artworks,onToggle}){return(<ul>{artworks.map(artwork=>(<likey={artwork.id}><label><inputtype="checkbox"checked={artwork.seen}onChange={e=>{onToggle(artwork.id,e.target.checked);}}/>{artwork.title}</label></li>))}</ul>);}
In general,you should only mutate objects that you have just created. If you were inserting anew artwork, you could mutate it, but if you’re dealing with something that’s already in state, you need to make a copy.
Write concise update logic with Immer
Updating nested arrays without mutation can get a little bit repetitive.Just as with objects:
- Generally, you shouldn’t need to update state more than a couple of levels deep. If your state objects are very deep, you might want torestructure them differently so that they are flat.
- If you don’t want to change your state structure, you might prefer to useImmer, which lets you write using the convenient but mutating syntax and takes care of producing the copies for you.
Here is the Art Bucket List example rewritten with Immer:
{"dependencies":{"immer":"1.7.3","react":"latest","react-dom":"latest","react-scripts":"latest","use-immer":"0.5.1"},"scripts":{"start":"react-scripts start","build":"react-scripts build","test":"react-scripts test --env=jsdom","eject":"react-scripts eject"},"devDependencies":{}}
Note how with Immer,mutation likeartwork.seen = nextSeen is now okay:
updateMyTodos(draft=>{
constartwork =draft.find(a=>a.id ===artworkId);
artwork.seen =nextSeen;
});This is because you’re not mutating theoriginal state, but you’re mutating a specialdraft object provided by Immer. Similarly, you can apply mutating methods likepush() andpop() to the content of thedraft.
Behind the scenes, Immer always constructs the next state from scratch according to the changes that you’ve done to thedraft. This keeps your event handlers very concise without ever mutating state.
Recap
- You can put arrays into state, but you can’t change them.
- Instead of mutating an array, create anew version of it, and update the state to it.
- You can use the
[...arr, newItem]array spread syntax to create arrays with new items. - You can use
filter()andmap()to create new arrays with filtered or transformed items. - You can use Immer to keep your code concise.
Challenge 1 of 4:Update an item in the shopping cart
Fill in thehandleIncreaseClick logic so that pressing ”+” increases the corresponding number:
import{useState}from'react';constinitialProducts =[{id:0,name:'Baklava',count:1,},{id:1,name:'Cheese',count:5,},{id:2,name:'Spaghetti',count:2,}];exportdefaultfunctionShoppingCart(){const[products,setProducts] =useState(initialProducts)functionhandleIncreaseClick(productId){}return(<ul>{products.map(product=>(<likey={product.id}>{product.name}{' '} (<b>{product.count}</b>)<buttononClick={()=>{handleIncreaseClick(product.id);}}> +</button></li>))}</ul>);}