10

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.
        },
    ]
  })
Emile Bergeron
  • 16,148
  • 4
  • 74
  • 121
MFA86
  • 105
  • 1
  • 1
  • 8
  • It would help if you provide how you are trying to set the state. – tsfahmad Jul 15 '20 at 15:48
  • 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. – Code-Apprentice Jul 15 '20 at 15:48
  • Does this answer your question? [Whats the best way to update an object in an array in ReactJS?](https://stackoverflow.com/questions/28121272/whats-the-best-way-to-update-an-object-in-an-array-in-reactjs) – Emile Bergeron Jul 15 '20 at 15:51
  • 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. – Emile Bergeron Jul 15 '20 at 15:52

6 Answers6

28

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.

D. Smith
  • 599
  • 3
  • 9
  • 1
    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});` – D. Smith Jul 16 '20 at 15:11
6

You need to use the spread operator like so:

const toggleDone = (id) => {
    let newState = [...state];
    newState[index].done = true;
    setState(newState])
}
JustCarty
  • 3,644
  • 5
  • 30
  • 49
TalOrlanczyk
  • 1,032
  • 5
  • 16
2

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
}
1
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);
}
glinda93
  • 5,694
  • 3
  • 29
  • 53
1

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
    }
})
moshfiqrony
  • 2,887
  • 15
  • 25
-2

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);
  };