Updating arrays in state
Add, remove, replace, insert, and sort items in a React state array without mutating it, and update objects inside arrays safely, in live examples.
- 01Pick array methods that are safe for stateTell the methods that change an array, like push, splice, and sort, from the ones that return a new array, like filter, map, and toSorted.
- 02Make every kind of list changeAdd, remove, replace, transform, insert, sort, and reverse items in an array in state by building a new array each time.
- 03Update objects inside an arrayChange one object in a list with map and spread, and explain why copying only the array still changes objects other code shares.
A shopping list that won't grow
An array is an object too, so everything in Updating objects in state applies to it. Arrays just have more ways to be changed by accident: many of their methods change the array they're called on. Here is a shopping list that adds eggs with push. Click Add eggs twice and watch the console under the result.
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Milk", "Bread"]); function addEggs() { items.push("Eggs"); setItems(items); console.log("items in the array:", items.length); } return ( <> <p>Shopping list: {items.join(", ")}</p> <button onClick={addEggs}>Add eggs</button> </> );}Line 4 keeps the list in state. Line 7 pushes "Eggs" onto the end of the array React already has, and line 8 hands React that same array. React compares it with what it has using Object.is, finds the same array, and skips the render. So line 14 keeps showing Milk, Bread, while line 9 prints items in the array: 3, then 4. Here is the fix:
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Milk", "Bread"]); function addEggs() { setItems([...items, "Eggs"]); } return ( <> <p>Shopping list: {items.join(", ")}</p> <button onClick={addEggs}>Add eggs</button> </> );}Line 7 builds a new array with spread: ...items copies every item into it, and "Eggs" goes at the end. React gets a different array and renders: Milk, Bread, Eggs. Every kind of list change in this lesson works the same way: read the old array, build a new one, and hand the new one to the set function.
filter leaves out Bread, the spread adds Tea, and toSorted sorts a copy. items, on the left, still holds Milk, Bread, and Eggs.A station doesn't scribble on the timetable on the wall. When a train changes, it prints a new timetable from the old one with the change in it, puts the new sheet up, and files the old one away exactly as it was. Anyone who looked at the old sheet still has the right picture of yesterday.
- In real life: The timetable on the wall
- In React: The array in state
- In real life: Crossing out a line with a pen
- In React:
items.push(…),items.splice(…),items.sort() - In real life: Printing a new timetable from the old one
- In React:
[...items, "Eggs"],items.filter(…),items.toSorted() - In real life: The station swaps the sheet on the wall
- In React: React renders with the new array
- In real life: Yesterday's sheet, filed away as it was
- In React: The old array, untouched
Treat an array in state as read-only. Don't call methods that change it, like push, splice, or sort, and don't assign to its items. Build a new array, with spread or with methods that return one, like filter, map, and toSorted, and pass that to the set function.
Which array methods change the array
Each row is one job. The middle column lists the ways that change the array they're called on: don't use them on an array in state. The right column lists the ways that leave it alone and give you a new array:
| Job | Changes the array (avoid on state) | Returns a new array (use on state) |
|---|---|---|
| Add an item | push, unshift | [...items, item], [item, ...items], concat |
| Remove an item | pop, shift, splice | filter, slice, toSpliced |
| Replace an item | items[index] = item, splice | map, with |
| Insert at a position | splice | [...items.slice(0, index), item, ...items.slice(index)], toSpliced |
| Sort | sort | toSorted, or [...items].sort() |
| Reverse | reverse | toReversed, or [...items].reverse() |
Watch out for slice and splice: the names are one letter apart, but slice copies part of an array into a new one, and splice removes or inserts items in the array itself. In React you'll use slice far more often. The Slicing, splicing and joining lesson covers both.
Four methods in the right column are newer: toSorted, toReversed, toSpliced, and with. Each is the copying version of a method that changes the array: sort, reverse, splice, and items[index] = value. They were added to JavaScript in 2023 (the ES2023 edition), and every current browser has them: Chrome and Edge since version 110, Firefox since 115, and Safari since 16. Run this:
const items = ["Milk", "Bread", "Eggs"];
console.log(items.toSorted().join(", "));
console.log(items.toReversed().join(", "));
console.log(items.with(1, "Tea").join(", "));
console.log(items.toSpliced(1, 1).join(", "));
console.log(items.join(", "));Line 3 prints Bread, Eggs, Milk, a sorted copy. Line 4 prints Eggs, Bread, Milk, a reversed copy. Line 5 prints Milk, Tea, Eggs: with(1, "Tea") is a copy with position 1 replaced (positions start at 0). Line 6 prints Milk, Eggs: toSpliced(1, 1) is a copy with one item removed at position 1. Line 7 prints Milk, Bread, Eggs: after all that, items hasn't changed. The Sorting and copying safely lesson covers these methods in full, with what to do for older browsers.
Try any method from the table. The playground runs it for real, in this browser, on a fresh ["Milk", "Bread", "Eggs"], and shows what it did to items and what it returned:
const items = ["Milk", "Bread", "Eggs"];const result = items.push("Tea");items changed- items now holds
["Milk", "Bread", "Eggs", "Tea"]- result
4- Is result the same array as items?
result isn't an array
push changed items itself, and returned 4. On an array in state, that's a mutation: copy first, or use a method that returns a new array.
Look at sort and reverse: they change items and return the same array. So setItems(items.sort()) looks like it passes a sorted list, but it passes the array React already has, changed. And push returns the new length, 4, not an array at all.
Adding and removing items
Real lists hold objects with an id, so each item has a stable key (the Rendering lists lesson explains why). Type an item, add it to either end, and remove a few:
import { useState } from "react"; let nextId = 3; export default function App() { const [name, setName] = useState(""); const [items, setItems] = useState([ { id: 0, name: "Milk" }, { id: 1, name: "Bread" }, { id: 2, name: "Eggs" }, ]); function addToEnd() { setItems([...items, { id: nextId++, name }]); setName(""); } function addToStart() { setItems([{ id: nextId++, name }, ...items]); setName(""); } function remove(id) { setItems(items.filter((item) => item.id !== id)); } return ( <> <label> Item <input value={name} onChange={(event) => setName(event.target.value)} /> </label> <button onClick={addToEnd} disabled={name === ""}>Add to end</button> <button onClick={addToStart} disabled={name === ""}>Add to start</button> <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => remove(item.id)}>Remove</button> </li> ))} </ul> </> );}Line 3 keeps a counter for new ids, outside the component, so it isn't reset on each render. Lines 7 to 11 keep the list. Line 14 adds to the end: a new array with every old item, then the new one. Line 19 adds to the start: the new item first, then ...items. Spread does the job of both push and unshift, without changing items.
Line 24 removes an item with filter: it builds a new array of the items whose id isn't the one you clicked. Type Tea and click Add to end, then remove Bread: the list shows Milk, Eggs, and Tea. The add buttons stay off while the text box is empty (disabled on lines 32 and 33).
Replacing and transforming items with map
To change some or all of the items, use map. It calls your function for every item and builds a new array from what the function returns. Return the item as it is to keep it, or a new item to replace it. Click +1 next to Tea:
import { useState } from "react"; export default function App() { const [cart, setCart] = useState([ { id: 0, name: "Tea", price: 3, quantity: 1 }, { id: 1, name: "Cake", price: 4, quantity: 2 }, { id: 2, name: "Lemonade", price: 2, quantity: 1 }, ]); function addOne(id) { const next = cart.map((item) => { if (item.id !== id) return item; return { ...item, quantity: item.quantity + 1 }; }); setCart(next); } function resetAll() { setCart(cart.map((item) => ({ ...item, quantity: 1 }))); } const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); return ( <> <ul> {cart.map((item) => ( <li key={item.id}> {item.name} × {item.quantity} <button onClick={() => addOne(item.id)}>+1</button> </li> ))} </ul> <p>Total: ${total}</p> <button onClick={resetAll}>Set every quantity to 1</button> </> );}addOne on lines 10 to 16 maps over the cart. Line 12 returns every other item as it is. Line 13 returns a new object for the item you clicked: ...item copies its fields, and the new quantity replaces the old one. Line 15 hands React the new array. The total on line 22 is worked out from the cart on every render, so it follows: Total: $13 becomes Total: $16.
map builds a new array. Line 13 makes a new object for Tea, and line 12 returns the Cake and Lemonade objects as they are.resetAll on lines 18 to 20 transforms every item: each one becomes a copy with quantity: 1. Click Set every quantity to 1: Total: $9. To replace the item at a known position instead of by id, with is shorter: items.with(1, "Tea") is a copy with Tea at position 1.
Inserting at a position
Spread puts a new item at the start or the end. To put one in the middle, take two slices of the old array: everything before the position, and everything from it on. Then spread both into a new array with the new item between them. Pick a position and click Insert:
import { useState } from "react"; let nextId = 3; export default function App() { const [position, setPosition] = useState(1); const [queue, setQueue] = useState([ { id: 0, title: "Morning Raga" }, { id: 1, title: "Chai Time" }, { id: 2, title: "Monsoon" }, ]); function insertSong() { const index = Math.min(Math.max(position, 0), queue.length); const song = { id: nextId, title: `New song ${nextId}` }; nextId++; setQueue([...queue.slice(0, index), song, ...queue.slice(index)]); } return ( <> <label> Position <input type="number" value={position} onChange={(event) => setPosition(Number(event.target.value))} /> </label> <button onClick={insertSong}>Insert</button> <ol> {queue.map((song) => ( <li key={song.id}>{song.title}</li> ))} </ol> </> );}Line 6 keeps the position from the number box on line 23, starting at 1. Line 14 keeps it between 0 and the length of the queue, so a position like -3 or 99 still works. Line 17 builds the new queue: queue.slice(0, index) is the songs before the position, then the new song, then queue.slice(index), the rest. Positions start at 0, so position 1 puts New song 3 second, after Morning Raga. Try position 0, then 99.
toSpliced does the same in one call: toSpliced(index, 0, song) removes 0 items at index and puts song there, in a copy. Run both:
const queue = ["Morning Raga", "Chai Time", "Monsoon"];
const withSlices = [...queue.slice(0, 1), "Rain Song", ...queue.slice(1)];
const withToSpliced = queue.toSpliced(1, 0, "Rain Song");
console.log(withSlices.join(", "));
console.log(withToSpliced.join(", "));
console.log(queue.join(", "));queue still holds three songs.Lines 6 and 7 print the same Morning Raga, Rain Song, Chai Time, Monsoon. Line 8 prints the queue itself, still three songs.
Sorting and reversing a copy
sort and reverse are the methods that most often change state by accident: they reorder the array itself, and return it. Sort a copy instead. Click Cheapest first, then Reverse:
import { useState } from "react"; export default function App() { const [products, setProducts] = useState([ { id: 0, name: "Cake", price: 4 }, { id: 1, name: "Lemonade", price: 2 }, { id: 2, name: "Tea", price: 3 }, ]); function cheapestFirst() { setProducts(products.toSorted((a, b) => a.price - b.price)); } function reverseOrder() { setProducts(products.toReversed()); } function cheapestFirstWithSort() { setProducts(products.sort((a, b) => a.price - b.price)); } return ( <> <button onClick={cheapestFirst}>Cheapest first</button> <button onClick={reverseOrder}>Reverse</button> <button onClick={cheapestFirstWithSort}>Cheapest first with sort()</button> <ol> {products.map((product) => ( <li key={product.id}> {product.name}: ${product.price} </li> ))} </ol> </> );}Line 11 uses toSorted with a function that compares prices (the Sorting and copying safely lesson explains the compare function): Lemonade: $2, Tea: $3, Cake: $4. Line 15 reverses a copy with toReversed: Cake, Tea, Lemonade. Before these methods, people wrote [...products].sort(…): copy, then sort the copy, which is local mutation and just as safe.
Now press Run again to start over, and click Cheapest first with sort(). Nothing changes: line 19 sorts the array in state itself, and sort returns that same array, so React skips the render. The page says Cake, Lemonade, Tea, but the array now holds Lemonade, Tea, Cake. Click Reverse: you'd expect the page reversed, Tea, Lemonade, Cake, but you get Cake, Tea, Lemonade, the reverse of the hidden sorted order.
Updating objects inside an array
Here is the bug that copying the array doesn't fix. An array holds references to its objects, not the objects themselves (the Objects and references lesson shows why). So [...list] is a new array whose slots point at the same objects. Change an object through the copy, and you've changed it everywhere it's used. Two friends keep lists of teas they've tried. Tick Green tea in Asha's list:
import { useState } from "react";import TeaList from "./TeaList.jsx"; const teas = [ { id: 0, name: "Green tea", tried: false }, { id: 1, name: "Masala chai", tried: false },]; export default function App() { const [ashaList, setAshaList] = useState(teas); const [benList, setBenList] = useState(teas); function toggle(list, setList, id) { const next = [...list]; const tea = next.find((item) => item.id === id); tea.tried = !tea.tried; setList(next); } return ( <> <h3>Asha has tried</h3> <TeaList teas={ashaList} onToggle={(id) => toggle(ashaList, setAshaList, id)} /> <h3>Ben has tried</h3> <TeaList teas={benList} onToggle={(id) => toggle(benList, setBenList, id)} /> </> );}Both lists start from the same teas on lines 4 to 7 (lines 10 and 11). The TeaList.jsx tab draws one list of checkboxes and calls onToggle with the tea's id. toggle on lines 13 to 18 looks careful: line 14 copies the array, and line 17 hands React the copy, so the page does update. But line 15 finds the Green tea object in the copy, which is the same object Ben's list holds, and line 16 changes it. Ben's Green tea is ticked too, though nobody touched his list.
[...list] copies the array, not the objects, so line 16 changes the Green tea object that both lists show.Two lists make the bug easy to see. With one list, it hides: the page looks right, but the old array's objects have changed, and anything still holding them, like an undo history or the previous render, now holds the new values. The fix is map, with a new object for the tea that changes:
import { useState } from "react";import TeaList from "./TeaList.jsx"; const teas = [ { id: 0, name: "Green tea", tried: false }, { id: 1, name: "Masala chai", tried: false },]; export default function App() { const [ashaList, setAshaList] = useState(teas); const [benList, setBenList] = useState(teas); function toggle(list, setList, id) { setList(list.map((tea) => (tea.id === id ? { ...tea, tried: !tea.tried } : tea))); } return ( <> <h3>Asha has tried</h3> <TeaList teas={ashaList} onToggle={(id) => toggle(ashaList, setAshaList, id)} /> <h3>Ben has tried</h3> <TeaList teas={benList} onToggle={(id) => toggle(benList, setBenList, id)} /> </> );}Line 14 maps over the list. For the tea you clicked, it returns { ...tea, tried: !tea.tried }, a new object; every other tea is returned as it is. Tick Green tea for Asha: only Asha's list changes. The rule from Updating objects in state holds here too: copy every level you change, which here is the array and the one tea object.
Immer works on arrays as well. Your recipe can call find, assign to an item, even push or splice on the draft, and produce builds the new array and the new tea object. This is the same toggle with Immer, loaded from esm.sh:
import { useState } from "react";import { produce } from "immer";import TeaList from "./TeaList.jsx"; const teas = [ { id: 0, name: "Green tea", tried: false }, { id: 1, name: "Masala chai", tried: false },]; export default function App() { const [ashaList, setAshaList] = useState(teas); const [benList, setBenList] = useState(teas); function toggle(list, setList, id) { setList( produce(list, (draft) => { const tea = draft.find((item) => item.id === id); tea.tried = !tea.tried; }), ); } return ( <> <h3>Asha has tried</h3> <TeaList teas={ashaList} onToggle={(id) => toggle(ashaList, setAshaList, id)} /> <h3>Ben has tried</h3> <TeaList teas={benList} onToggle={(id) => toggle(benList, setBenList, id)} /> </> );}Lines 17 and 18 find the tea in the draft and flip tried, just like the buggy version, but on the draft. Immer makes a new array and a new Green tea object, and Ben's list keeps its own.
A shopping cart with every kind of update
A cart page uses almost every update from this lesson. Add a few products (twice for one of them), sort, and remove one:
import { useState } from "react"; const shelf = [ { id: "tea", name: "Tea", price: 3 }, { id: "cake", name: "Cake", price: 4 }, { id: "lemonade", name: "Lemonade", price: 2 },]; export default function App() { const [cart, setCart] = useState([]); function add(product) { if (cart.some((line) => line.id === product.id)) { setCart(cart.map((line) => (line.id === product.id ? { ...line, quantity: line.quantity + 1 } : line))); } else { setCart([...cart, { ...product, quantity: 1 }]); } } function remove(id) { setCart(cart.filter((line) => line.id !== id)); } function sortByName() { setCart(cart.toSorted((a, b) => a.name.localeCompare(b.name))); } const total = cart.reduce((sum, line) => sum + line.price * line.quantity, 0); return ( <> {shelf.map((product) => ( <button key={product.id} onClick={() => add(product)}> Add {product.name} </button> ))} <ul> {cart.map((line) => ( <li key={line.id}> {line.name} × {line.quantity} <button onClick={() => remove(line.id)}>Remove</button> </li> ))} </ul> <p>Total: ${total}</p> <button onClick={sortByName}>Sort by name</button> </> );}add on lines 12 to 18 checks with some whether the product is already in the cart. If it is, line 14 maps over the cart and gives that line a new object with one more in quantity. If not, line 16 adds a new line to the end with spread: the product's fields, plus quantity: 1. Line 21 removes with filter, line 25 sorts a copy by name with toSorted, and line 28 adds up the total from the cart on every render.
Click Add Tea, Add Cake, and Add Tea again: Tea × 2 and Cake × 1, Total: $10. Sort by name puts Cake first. Remove Tea, and the total is $4. Not one line changes cart or an object in it.
setItems([...items, "Tea"]);items.push("Tea"); setItems(items);setItems(items.filter((item) => item !== "Milk"));items.splice(0, 1); setItems([...items]);setItems(items.toSorted());setItems(items.sort());setItems([...items].reverse());items[0] = "Tea"; setItems([...items]);const next = [...cart]; next[0].quantity = 2; setCart(next);setCart(cart.map((line, index) => (index === 0 ? { ...line, quantity: 2 } : line)));
Sort each click handler body: does it build a new array and leave everything in state alone, or does it change the array or an object in it?
Common mix-ups
- "
setItems(items.sort())passes a sorted list." No. It passes the same array, sorted in place. React sees the array it already has and skips the render. Useitems.toSorted(). - "
sliceandspliceare the same." No.slicecopies;splicechanges the array.toSplicedis the copyingsplice. - "
[...items]copies the objects too." No. It copies the array. Its slots point at the same objects, so changing one through the copy changes it in state. - "If the page updates, the update was safe." No. Copy the array, then change an object in it, and the page updates while the old state's objects change too.
- "You can never call
sortorpushin React." No. On an array you just made, like[...items], they're fine: that's local mutation. The rule is for arrays already in state. - "
mapmakes a copy of every object." No. It makes a new array of whatever your function returns. Return the item as it is, and it's the same object.
Practice exercises
Each exercise shows its code's result under it, or when you press Edit & run. Change the code, and the result updates as you type. Try a guess first, then use the hints.
What does this code print?
const items = ["Milk", "Bread", "Eggs"];
const reversed = items.reverse();
console.log(items.join(", "));const items = ["Milk", "Bread", "Eggs"];
const reversed = items.toReversed();
console.log(items.join(", "));reverse reorders items itself and returns that same array, so line 3 prints Eggs, Bread, Milk. With toReversed, as in this solution, items keeps its order and prints Milk, Bread, Eggs.
What does the page show after one click on Remove last? Answer first, then press Edit & run to check.
import { useState } from "react";
export default function App() {
const [items, setItems] = useState(["Milk", "Bread", "Eggs"]);
function removeLast() {
items.pop();
setItems(items);
}
return (
<>
<p>{items.join(", ")}</p>
<button onClick={removeLast}>Remove last</button>
</>
);
}import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Milk", "Bread", "Eggs"]); function removeLast() { setItems(items.slice(0, -1)); } return ( <> <p>{items.join(", ")}</p> <button onClick={removeLast}>Remove last</button> </> );}pop removes Eggs from the array in state, and setItems(items) hands React that same array, so the page still says Milk, Bread, Eggs. items.slice(0, -1), in this solution, is a new array without the last item, and the page updates.
Remove does nothing on the page. Fix remove, then remove Bread. What does the list show?
import { useState } from "react"; export default function App() { const [items, setItems] = useState([ { id: 0, name: "Milk" }, { id: 1, name: "Bread" }, { id: 2, name: "Eggs" }, ]); function remove(index) { items.splice(index, 1); setItems(items); } return ( <ul> {items.map((item, index) => ( <li key={item.id}> {item.name} <button onClick={() => remove(index)}>Remove</button> </li> ))} </ul> );}import { useState } from "react"; export default function App() { const [items, setItems] = useState([ { id: 0, name: "Milk" }, { id: 1, name: "Bread" }, { id: 2, name: "Eggs" }, ]); function remove(id) { setItems(items.filter((item) => item.id !== id)); } return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => remove(item.id)}>Remove</button> </li> ))} </ul> );}filter builds a new array without the item whose id you clicked, and leaves items alone. React gets a new array and renders. Passing the id instead of the position also keeps the button right if the list is reordered.
This toggle works on the page, but it changes a to-do object in state. Rewrite toggle with map. When you tick Buy tea, how many new to-do objects does your version make?
import { useState } from "react"; export default function App() { const [todos, setTodos] = useState([ { id: 0, text: "Buy tea", done: false }, { id: 1, text: "Bake a cake", done: false }, { id: 2, text: "Call Asha", done: false }, ]); function toggle(id) { const next = [...todos]; const todo = next.find((item) => item.id === id); todo.done = !todo.done; setTodos(next); } return ( <ul> {todos.map((todo) => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.done} onChange={() => toggle(todo.id)} /> {todo.text} </label> </li> ))} </ul> );}import { useState } from "react"; export default function App() { const [todos, setTodos] = useState([ { id: 0, text: "Buy tea", done: false }, { id: 1, text: "Bake a cake", done: false }, { id: 2, text: "Call Asha", done: false }, ]); function toggle(id) { setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo))); } return ( <ul> {todos.map((todo) => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.done} onChange={() => toggle(todo.id)} /> {todo.text} </label> </li> ))} </ul> );}map returns the to-dos you didn't click as they are, and a new object only for the one you ticked. So ticking Buy tea makes one new to-do object and one new array; the old objects are never changed.
Make Play Rain Song next put Rain Song right after the first song, with toSpliced. After one click, what is second in the queue?
import { useState } from "react"; export default function App() { const [queue, setQueue] = useState(["Morning Raga", "Chai Time", "Monsoon"]); function playNext() { // Put "Rain Song" right after the first song, with toSpliced. } return ( <> <button onClick={playNext}>Play Rain Song next</button> <ol> {queue.map((title) => ( <li key={title}>{title}</li> ))} </ol> </> );}import { useState } from "react"; export default function App() { const [queue, setQueue] = useState(["Morning Raga", "Chai Time", "Monsoon"]); function playNext() { setQueue(queue.toSpliced(1, 0, "Rain Song")); } return ( <> <button onClick={playNext}>Play Rain Song next</button> <ol> {queue.map((title) => ( <li key={title}>{title}</li> ))} </ol> </> );}toSpliced(1, 0, "Rain Song") removes nothing at position 1 and puts Rain Song there, in a new array. The queue becomes Morning Raga, Rain Song, Chai Time, Monsoon, and queue itself is never changed.
Write moveUp so it swaps an item with the one above it, using with. Click Move up on Tea. What is the order now?
import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Cake", "Lemonade", "Tea"]); function moveUp(index) { if (index === 0) return; // Swap items[index] and items[index - 1], without changing items. } return ( <ol> {items.map((item, index) => ( <li key={item}> {item} <button onClick={() => moveUp(index)}>Move up</button> </li> ))} </ol> );}import { useState } from "react"; export default function App() { const [items, setItems] = useState(["Cake", "Lemonade", "Tea"]); function moveUp(index) { if (index === 0) return; setItems(items.with(index - 1, items[index]).with(index, items[index - 1])); } return ( <ol> {items.map((item, index) => ( <li key={item}> {item} <button onClick={() => moveUp(index)}>Move up</button> </li> ))} </ol> );}The first with makes a copy with Tea in Lemonade's place; the second makes a copy of that with Lemonade in Tea's old place. Both read from items, which never changes. Move up on Tea gives Cake, Tea, Lemonade.
Check your understanding
For each question, ask: does this make a new array, and does it change the old array or an object in it? Press Edit & run on a question's code to check your answer.
Question 1 of 7Which call leaves
itemsexactly as it was?Choose an answer to see the explanation.
Question 2 of 7After one click on Add eggs, what does this list show?
Read the code, then predictimport { useState } from "react"; export default function App() { const [items, setItems] = useState(["Milk", "Bread"]); return ( <> <p>{items.join(", ")}</p> <button onClick={() => { items.push("Eggs"); setItems(items); }} > Add eggs </button> </> ); }Choose an answer to see the explanation.
Question 3 of 7What's the difference between
sliceandsplice?Choose an answer to see the explanation.
Question 4 of 7What does this insertion print?
Read the code, then predictconst items = ["Milk", "Bread", "Eggs"]; const next = [...items.slice(0, 1), "Tea", ...items.slice(1)]; console.log(next.join(", "));Choose an answer to see the explanation.
Question 5 of 7What does this copied list print?
Read the code, then predictconst list = [{ name: "Green tea", tried: false }]; const copy = [...list]; copy[0].tried = true; console.log(list[0].tried);Choose an answer to see the explanation.
Question 6 of 7You need to mark the to-do with this
idas done. Which update is right?Choose an answer to see the explanation.
Question 7 of 7What does
withleave initems?Read the code, then predictconst items = ["Milk", "Bread", "Eggs"]; const next = items.with(1, "Tea"); console.log(items.join(", "));Choose an answer to see the explanation.
Key takeaways
- An array in state is read-only.
push,pop,splice,sort,reverse, anditems[index] = …change it; don't use them on it. - Add with spread, remove with
filter, replace or transform withmap, and insert with twoslices ortoSpliced. - Sort and reverse a copy:
toSortedandtoReversed(in every current browser), or[...items].sort(). - Copying the array doesn't copy its objects. To change an object in a list,
mapto a new object for that item and keep the rest. - Immer works on arrays too: change the draft with any method, and
producebuilds the new array.
Remember the one-liner.
Read the old array, build a new one, and never change an item in place.
Coming next: Reacting to input with state. It starts the Managing state stage: describe each visual state of a component, and let state choose between them.