How to update state with usestate in an array of objects?
Asked Answered
A

7

21

I'm having some trouble with the React useState hook. I have a todolist with a checkbox button and I want to update the 'done' property to 'true' that has the same id as the id of the 'clicked' checkbox button. If I console.log my 'toggleDone' function it returns the right id. But I have no idea how I can update the right property.

The current state:

const App = () => {

  const [state, setState] = useState({
    todos: 
    [
        {
          id: 1,
          title: 'take out trash',
          done: false
        },
        {
          id: 2,
          title: 'wife to dinner',
          done: false
        },
        {
          id: 3,
          title: 'make react app',
          done: false
        },
    ]
  })

  const toggleDone = (id) => {
    console.log(id);
}

  return (
    <div className="App">
        <Todos todos={state.todos} toggleDone={toggleDone}/>
    </div>
  );
}

The updated state I want:

const App = () => {

  const [state, setState] = useState({
    todos: 
    [
        {
          id: 1,
          title: 'take out trash',
          done: false
        },
        {
          id: 2,
          title: 'wife to dinner',
          done: false
        },
        {
          id: 3,
          title: 'make react app',
          done: true // if I checked this checkbox.
        },
    ]
  })
Artemis answered 15/7, 2020 at 15:45 Comment(4)
It would help if you provide how you are trying to set the state.Valuable
You need to call setState() with the modified state. Have you tried something? If so, what was the result? If you are struggling with how to even start, check out the map() function.Doretha
Does this answer your question? Whats the best way to update an object in an array in ReactJS?Dressing
Also, with hooks, there's no need to nest the array inside an object. You can call useState multiple times to manage different state values separately.Dressing
D
43

You can safely use javascript's array map functionality since that will not modify existing state, which react does not like, and it returns a new array. The process is to loop over the state's array and find the correct id. Update the done boolean. Then set state with the updated list.

const toggleDone = (id) => {
  console.log(id);

  // loop over the todos list and find the provided id.
  let updatedList = state.todos.map(item => 
    {
      if (item.id == id){
        return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
      }
      return item; // else return unmodified item 
    });

  setState({todos: updatedList}); // set state to new object with updated list
}

Edit: updated the code to toggle item.done instead of setting it to true.

Debug answered 15/7, 2020 at 16:1 Comment(3)
I want to point out something in the answer @bravemaster posted. They spread the state with {...state, todos: [...state.todos]} which is good practice. With my solution, if you were to include anything other than todos in the state, it would be lost in the setState operation (it works fine now since all you have in state is the todos object). The way to keep all other state and update todos at the same time would be setState({...state, todos: updatedList});Debug
Create answer. I have been using this pattern. But I have a question about the cost. Using the map loop imposes a linear cost on this pattern making it not effective in large arrays. Is there another pattern that makes use of the array's index feature?Karyotype
In this case the state todos is an array, so the linear search is an O(n) operation. I don't know of any other specific patterns for this case, but maybe there are other things we can try. If we assume that the id's are incremental as in the example, taking the first and last as boundaries we can implement a binary search, or I think the easier route is to convert the todos array to an object where the id's are keys, and we can jump right to the one we need to modify. Either way, remember that we need to make a copy and not modify the original state, which the map function also does for us.Debug
U
9

You need to use the spread operator like so:

const toggleDone = (id) => {
    let newState = [...state];
    newState[index].done = true;
    setState(newState])
}
Urbanity answered 15/7, 2020 at 15:58 Comment(5)
This also mutates the current state, which is an anti-pattern in React.Dressing
@EmileBergeron is creating a copy of the state first in newStateSheet
@FacundoColombier it's only a shallow copy. The objects inside the array are the same as in the original state.Dressing
with the spread operator [...state] is actually making a copy, not linked to the original, so no state is being modified "anti-pattern"Sheet
Emile is correct here. [...state] will only make a shallow copy. You have to make a second copy like: newState[index] = {... newState[index], done: true}Wavawave
G
9

D. Smith's answer is great, but could be refactored to be made more declarative like so..

const toggleDone = (id) => {
 console.log(id);
 setState(state => {
     // loop over the todos list and find the provided id.
     return state.todos.map(item => {
         //gets everything that was already in item, and updates "done" 
         //else returns unmodified item
         return item.id === id ? {...item, done: !item.done} : item
     })
 }); // set state to new object with updated list
}
Gennagennaro answered 20/12, 2021 at 15:22 Comment(1)
The problem here is that after, instead of state being an object with a todos property which contains an array, state would contain the array directly. Instead, you could use the spread syntax in your return statement, like return {...state, todos: state.todos.map(.Elfrieda
A
6

Something similar to D. Smith's answer but a little more concise:

const toggleDone = (id) => {

  setState(prevState => {
            // Loop over your list
            return prevState.map((item) => {
                // Check for the item with the specified id and update it
                return item.id === id ? {...item, done: !item.done} : item
            })
        })
}
Appearance answered 6/6, 2022 at 0:32 Comment(2)
This seems to be the same as Sam Kingston's answer.Dressing
if you want to be concise, you don't need the return at all.: setState( prevState => prevState.map(item => item.id ? { ...item, done: !item.done} ? item ) )Wavawave
Z
4
const toggleDone = (id) => {
    console.log(id);
    // copy old state
    const newState = {...state, todos: [...state.todos]};
    // change value
    const matchingIndex = newState.todos.findIndex((item) => item.id == id);
    if (matchingIndex !== -1) {
       newState.todos[matchingIndex] = {
           ...newState.todos[matchingIndex], 
           done: !newState.todos[matchingIndex].done 
       }
    }
    // set new state
    setState(newState);
}
Zetland answered 15/7, 2020 at 15:52 Comment(0)
P
2

All the great answers but I would do it like this

setState(prevState => {
    ...prevState,
    todos: [...prevState.todos, newObj]
})

This will safely update the state safely. Also the data integrity will be kept. This will also solve the data consistency at the time of update.

if you want to do any condition do like this

setState(prevState => {
    if(condition){
        return {
            ...prevState,
            todos: [...prevState.todos, newObj]
        }
    }else{
        return prevState
    }
})
Pauperism answered 20/8, 2021 at 17:34 Comment(1)
where i will check the condition e.g update a nested item when condition is true other wise sameWieche
H
-1

I would create just the todos array using useState instead of another state, the key is creating a copy of the todos array, updating that, and setting it as the new array. Here is a working example: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      title: "take out trash",
      done: false
    },
    {
      id: 2,
      title: "wife to dinner",
      done: false
    },
    {
      id: 3,
      title: "make react app",
      done: false
    }
  ]);

  const toggleDone = (e, item) => {
    const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
    const updatedTodos = [...todos]; // creates a copy of the array

    updatedTodos[indexToUpdate].done = !item.done;
    setTodos(updatedTodos);
  };
Hoeg answered 24/3, 2021 at 0:53 Comment(1)
You have the same issue as the first reply. You need to create a copy like: updatedTodos[indexToUpdate].done = {...item, done: !item.done;}Wavawave

© 2022 - 2025 — McMap. All rights reserved.