18

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
  • 17,074
  • 5
  • 83
  • 129
MFA86
  • 185
  • 1
  • 1
  • 9
  • 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

7 Answers7

42

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
  • 739
  • 5
  • 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
9

You need to use the spread operator like so:

const toggleDone = (id) => {
    let newState = [...state];
    newState[index].done = true;
    setState(newState])
}
JustCarty
  • 3,839
  • 5
  • 31
  • 51
TalOrlanczyk
  • 1,205
  • 7
  • 21
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
}
Sam Kingston
  • 138
  • 1
  • 5
  • 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(`. – Neil Haskins Oct 05 '22 at 18:50
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
            })
        })
}
Henry Ecker
  • 34,399
  • 18
  • 41
  • 57
AlmightyL0rd
  • 71
  • 1
  • 1
  • This seems to be the same as [Sam Kingston's answer](https://stackoverflow.com/a/70424055/1218980). – Emile Bergeron Jul 11 '22 at 15:17
  • 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 ) )` – JohnFlux Oct 25 '22 at 21:10
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);
}
glinda93
  • 7,659
  • 5
  • 40
  • 78
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
    }
})
moshfiqrony
  • 4,303
  • 2
  • 20
  • 29
-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);
  };
  • You have the same issue as the first reply. You need to create a copy like: `updatedTodos[indexToUpdate].done = {...item, done: !item.done;}` – JohnFlux Oct 25 '22 at 21:10