I want to know how to not mutate state in the following scenario:
I'm using React Hooks. I have a TodoList
that lists TodoItems
. TodoItems
can be clicked to run a function called toggle
which toggles their completed
property.
import React, { useState } from "react";
import TodoItem from "./TodoItem";
export default function TodoList() {
const [todos, setTodos] = useState([
{ text: "Example", completed: false }
]);
const toggle = index => {
const newTodos = [...todos];
newTodos[index].completed = !newTodos[index].completed; // Mutating state?
setTodos(newTodos);
};
return (
<>
{todos.map((item, index) => (
<TodoItem key={index} index={index} todo={item} toggle={toggle} />
))}
</>
);
}
I was told that this was mutating state:
You create a copy of the todos array, but you still mutate an item inside the array:
newTodos[index].completed = !newTodos[index].completed;
Spreading the array only creates a shallow copy of the array, but the original todo object is not copied and thus mutated.
So, what is the correct way to handle this without mutating state? I've tried passing in a prevTodos
param, but then the state doesn't update:
const toggle = index => {
setTodos(prevTodos => {
prevTodos[index].completed = !prevTodos[index].completed;
return prevTodos;
});
};
Update: I've taken a look at the posts you've all recommended. This is a solution, although I'm wondering if there's a cleaner way to express it:
const toggle = index => {
setTodos(
todos.map((todo, i) =>
i === index ? { ...todo, completed: !todo.completed } : todo
)
);
};
Update 2: Thanks everyone. I posted the solution below. StackOverflow won't let me accept it for 2 days, but I'll do it then. The accepted answer on the suggested post suggests the use of a third-party library, and it doesn't use React Hooks, so I don't consider this question a duplicate.