React State with Arrays
Table of Contents + −
In the last lesson, React State with Objects, you held a whole object in state and updated it without changing the old one. Now let’s do the same thing with a list, like a to-do list or a chat or a cart.
🤔 Why do we need arrays in state?
Most real apps don’t show just one thing. They show a list of things, and that list keeps changing.
- A to-do app shows many tasks. You add one, you delete one, you tick one as done.
- A chat shows many messages. Each new message gets added to the bottom.
- A cart shows many products. You add items, you remove items, you change quantities.
So you need a place to keep that whole list. That place is state, and the value you put there is an array.
This is how you set up a list in state. The starting value is just an empty array.
import { useState } from "react";
function TodoList() { const [todos, setTodos] = useState([]); // todos -> the current list // setTodos -> the function to give React a new list}So todos holds the list and setTodos is how you update it. The empty [] means we start with no tasks.
🐍 What does “do not mutate” mean?
Here is the rule that trips up almost everyone. You must never change the array that’s already in state. You make a new array instead.
- “Mutate” just means to change a thing in place, without making a copy.
push,splice, and writing to an index liketodos[0] = ...all change the same array. So that’s mutating.- React decides whether to re-render by checking if the array is a different array than before. So if you mutate, it’s still the same array, and React often shows nothing changed.
A quick picture. Think of the list as a box. Mutating means reaching into the same box and moving things around, so React sees the same box label and walks away. The right way is to make a fresh box with the new contents, then hand React the fresh box.
const [todos, setTodos] = useState(["Buy milk"]);
// ❌ Wrong - push changes the SAME array, React may not re-rendertodos.push("Walk dog");setTodos(todos);
// ✅ Right - make a NEW array, React sees a new value and re-renderssetTodos([...todos, "Walk dog"]);So both lines add “Walk dog”. But only the second one gives React a brand-new array, so only the second one updates the screen reliably.
➕ Adding an item with spread
When you want to add something to the list, you build a new array that has the old items plus the new one.
- The
...todospart is the spread operator. So it copies every item from the old list into the new one. - Then you put the new item right after it.
- So
[...todos, newItem]reads as “all the old todos, then the new todo”.
Here a button adds a new task to the list each time it’s clicked.
function TodoList() { const [todos, setTodos] = useState([]);
function addTodo() { const newTodo = { id: Date.now(), text: "New task", done: false }; setTodos([...todos, newTodo]); // old items + new one }
return ( <div> <button onClick={addTodo}>Add task</button> <p>You have {todos.length} tasks.</p> </div> );}So every click builds a fresh array with one more item, and the count on screen goes up.
New item goes where you put it
Want the new item at the top instead of the bottom? Just flip the order: setTodos([newTodo, ...todos]). The spread copies the old list either way.
➖ Removing an item with filter
To remove something, you don’t delete from the old list. You build a new list that simply leaves the unwanted item out.
filterwalks through every item and keeps only the ones that pass a test.- The test here is “keep this todo only if its id is not the one I want to remove”.
- So the item you’re removing fails the test, and it’s left out of the new array.
Here we delete a task by its id using filter.
function deleteTodo(id) { // Keep every todo whose id is NOT the one we are removing setTodos(todos.filter((todo) => todo.id !== id));}So filter gives back a brand-new array without that one item, and React re-renders the shorter list.
Why filter and not splice?
splice changes the original array in place, that’s mutating. filter never touches the old array, it returns a new one. So always reach for filter when removing from state.
✏️ Updating one item with map
Updating is the tricky one. You want to change a single item but keep all the others exactly as they are.
mapwalks through every item and gives back a new array of the same length.- For the item you want to change, you return a new object with the change applied.
- For every other item, you return it as it is, unchanged.
Here we mark one task as done by its id using map.
function markDone(id) { setTodos( todos.map((todo) => todo.id === id ? { ...todo, done: true } : todo ) );}Let’s read that line by line.
maplooks at each todo, one at a time.- If the id matches, it returns
{ ...todo, done: true }, which is a copy of that todo withdoneflipped totrue. - If the id does not match, it just returns the same
todountouched. - So you get a new array where exactly one item changed.
Spread the object too
Notice the { ...todo, done: true } inside. You copy the old todo first, then overwrite one field. This is the object-update trick from the last lesson, used right inside the array update. You’re being immutable at both levels.
🌍 The three moves together
These three patterns cover almost everything you’ll do with a list in state. It helps to see them side by side.
- Add -> spread the old list and tack on the new item:
setTodos([...todos, newItem]). - Remove -> filter out the one you don’t want:
setTodos(todos.filter((t) => t.id !== id)). - Update -> map over the list and change only the matching one:
setTodos(todos.map((t) => t.id === id ? { ...t, done: true } : t)).
Here are all three sitting in one component so you can see how they fit.
import { useState } from "react";
function TodoList() { const [todos, setTodos] = useState([]);
const addTodo = () => setTodos([...todos, { id: Date.now(), text: "New task", done: false }]);
const deleteTodo = (id) => setTodos(todos.filter((todo) => todo.id !== id));
const markDone = (id) => setTodos( todos.map((todo) => (todo.id === id ? { ...todo, done: true } : todo)) );
return ( <ul> {todos.map((todo) => ( <li key={todo.id}> {todo.done ? "✅ " : ""} {todo.text} <button onClick={() => markDone(todo.id)}>Done</button> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </li> ))} </ul> );}So the same map, filter, and spread you already know from plain JavaScript do all the work. The only new habit is to always pass the result to setTodos, never change the old array yourself.
They never touch the original
map, filter, and spread all return a brand-new array and leave the old one alone. That’s exactly why React likes them. push, splice, sort, and reverse change the original, so avoid those on state, or copy first with [...todos].
🧩 What You’ve Learned
- ✅ A list in React lives in state, set up with
const [todos, setTodos] = useState([]) - ✅ Never mutate the array in state with
push,splice, or index assignment, React may not re-render - ✅ Always build a new array and pass it to the setter so React sees a fresh value
- ✅ Add an item by spreading the old list:
setTodos([...todos, newItem]) - ✅ Remove an item with
filter:setTodos(todos.filter((t) => t.id !== id)) - ✅ Update one item with
map, returning a copy for the match and the original for the rest - ✅
map,filter, and spread are safe because they return new arrays and leave the old one alone
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Why should you avoid using push to add an item to an array in state?
Why: push mutates the existing array. React compares by identity, sees the same array, and often skips the re-render. Build a new array with spread instead.
- 2
Which line correctly adds newItem to the end of the todos list in state?
Why: [...todos, newItem] copies the old items into a new array and adds the new one at the end, then hands that new array to setTodos.
- 3
How do you remove the todo with a given id from state?
Why: filter returns a new array that keeps only the todos whose id does not match, leaving the original array untouched.
- 4
When updating one item with map, what should you return for the items that should NOT change?
Why: map must return a value for every item. For the ones you are not changing, return the item as it is so the rest of the list stays exactly the same.
🚀 What’s Next?
You can now hold a whole list in state and add, remove, and update items the right way. Next you’ll see what to do when one component needs more than one piece of state at the same time.