Toggling a value in object (React)

How do I toggle the “done” to true by using setState() of a specific item? For example, if by clicking a button or checkbox, I want to toggle that item’s state “done” to true by using a eventHandler. But I don’t know how to setState such that only that item changes to "done":true.

  this.state = {
      "todo": [
        { "id": 1, "item": "Walk The Dog", "done": false },
        { "id": 2, "item": "Drink more water", "done": false },
        { "id": 3, "item": "Brush teeth", "done": false }],
      "userInput": ""
    }

I need to use ...this.state.todo instead because my function is outside the constructor function where state lives, but for some reason I get:

TypeError: Cannot read property 'state' of undefined
toggleDone
src/App.js:36

  35 | toggleDone(e) {
> 36 |   const updatedToDo = [...this.state.todo];
     | ^  37 |   updatedToDo.map(item => item.id === e.id ? Object.assign(item, { done: true }) : item);
  38 |   this.setState({ todo: updatedToDo });

Where toggleDone(e) is an event listener.

Funny, I had a very similar issue earlier today and learned that you can use prevState for these situations.

Try this:

  toggleTodo() {
    this.setState(prevState => ({
      todo: {...prevState.todo, [0]: {...prevState.todo[0], 'done': true} }
    }))
  }

In this example, it would change the first object (since it’s using [0]).

Since you probably want to do it dynamically, instead of [0], you’d want to have a variable that gets the index of the obj in the todo array you want to change, and use that instead.

1 Like

I’ll give prevState a try.

@camperextraordinaire Sorry about that - here is my full code

import React from 'react';
import './App.css';

function Todolist(props) {

  let newArray = props.todos.map(x =>
    <div className="input-group">
      <div className="input-group-prepend">
        <div className="input-group-text">
          <input type="checkbox" id={props.todos.id} defaultChecked={x.done} onClick={props.toggleDone} aria-label="Checkbox for following text input" />
        </div>
      </div>
      <input type="text" value={x.item} className="form-control" aria-label="Text input with checkbox" />
    </div>)
  return newArray;
}

class ToDoItems extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      "todo": [
        { "id": 1, "item": "Walk The Dog", "done": false },
        { "id": 2, "item": "Drink more water", "done": false },
        { "id": 3, "item": "Brush teeth", "done": false }],
      "userInput": ""
    }
    this.handleChange = this.handleChange.bind(this);
    this.addItem = this.addItem.bind(this);
    this.deleteItem = this.deleteItem.bind(this);
    this.clearList = this.clearList.bind(this);
  }

  toggleDone(e) {
    const updatedToDo = [...this.state.todo]
      .map(item => item.id === e.id ? Object.assign(item, { done: true }) : item);
    this.setState({ todo: updatedToDo });

  }
  handleChange(e) {
    this.setState({
      "userInput": e.target.value
    });
  }

  addItem() {
    let newId = this.state.todo.length + 1;
    if (this.state.userInput !== "") {
      this.setState({
        "todo": [...this.state.todo, { "id": newId, "item": this.state.userInput, "done": false }], "userInput": ""
      })
    }
  }

  deleteItem() {
    let filteredArray = this.state.todo.filter(x => x.done);
    console.log(this.state.todo);
    console.log(filteredArray);
    this.setState({
      "todo": filteredArray,
      "userInput": ""
    })

  }


  clearList() {
    let message = "Are you sure you want to delete all items?"
    let result = window.confirm(message);
    if (result) {
      this.setState({
        "todo": [
          { "item": "Walk The Dog", "done": false },
          { "item": "Drink more water", "done": false },
          { "item": "Brush teeth", "done": false }],
        "userInput": ""
      })
    }
  }

  render() {
    let formStyle = { width: "100%" }
    return (
      <div className="App">
        <div className="todobox">

          <h1>To-Do List App</h1>

          <div className="buttonContainer">
            <button className="btn btn-outline-primary btn-sm" type='button' onClick={this.addItem}>Add Item</button>
            <button className="btn btn-outline-warning btn-sm" type='button' onClick={this.deleteItem}>Delete Item</button>
            <button className="btn btn-outline-danger btn-sm" type='button' onClick={this.clearList}>Clear List</button>
          </div>

          <div className="itemlist">
            <form style={formStyle}>
              <input className="userInput" value={this.state.userInput} onChange={this.handleChange} placeholder=" Enter New Item" type="text"></input>
            </form>
            <Todolist todos={this.state.todo} toggleDone={this.toggleDone} listLength={this.state.todo.length} />
          </div>
        </div>
      </div >
    )
  }


}

export default ToDoItems;

Note that it’s generally easier to use a map instead of a list if you need to access and update specific items, either using a plain object or an actual Map, keyed by ID.

state = {
  todos: {
    1: { "id": 1, "item": "Walk The Dog", "done": false },
    2: { "id": 2, "item": "Drink more water", "done": false },
    3: { "id": 3, "item": "Brush teeth", "done": false },
  },
  "userInput": ""
}

Or

state = {
  todos: new Map([
    [1, { "id": 1, "item": "Walk The Dog", "done": false }],
    [2, { "id": 2, "item": "Drink more water", "done": false }],
    [3, { "id": 3, "item": "Brush teeth", "done": false }],
  ]),
  "userInput": ""
}

(Otherwise, either of the suggestions using the function argument to setState or filtering are good)

1 Like

Thank you, @camperextraordinaire ! I have changed the code to match your recommendations, but whenever I click on a check box, I still get the same error as comment 2/8 above:

ypeError: Cannot read property 'state' of undefined
 33 | }
  34 | 
  35 | toggleDone(e) {
> 36 |   const updatedToDo = [...this.state.todo].map(item => item.id == e.target.id ? Object.assign(item, { done: true }) : item);
     | ^  37 |   this.setState({ todo: updatedToDo });
  38 | 
  39 | }

Here’s my updated full code:

import React from 'react';
import './App.css';

function Todolist(props) {

  let newArray = props.todos.map(x =>
    <div className="input-group">
      <div className="input-group-prepend">
        <div className="input-group-text">
          <input type="checkbox" id={x.id} defaultChecked={x.done} onClick={props.toggleDone} aria-label="Checkbox for following text input" />
        </div>
      </div>
      <input type="text" value={x.item} className="form-control" aria-label="Text input with checkbox" />
    </div>)
  return newArray;
}

class ToDoItems extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      "todo": [
        { "id": 1, "item": "Walk The Dog", "done": false },
        { "id": 2, "item": "Drink more water", "done": false },
        { "id": 3, "item": "Brush teeth", "done": false }],
      "userInput": ""
    }
    this.handleChange = this.handleChange.bind(this);
    this.addItem = this.addItem.bind(this);
    this.deleteItem = this.deleteItem.bind(this);
    this.clearList = this.clearList.bind(this);
  }

  toggleDone(e) {
    const updatedToDo = [...this.state.todo].map(item => item.id == e.target.id ? Object.assign(item, { done: true }) : item);
    this.setState({ todo: updatedToDo });

  }
  handleChange(e) {
    this.setState({
      "userInput": e.target.value
    });
  }

  addItem() {
    let newId = this.state.todo.length + 1;
    if (this.state.userInput !== "") {
      this.setState({
        "todo": [...this.state.todo, { "id": newId, "item": this.state.userInput, "done": false }], "userInput": ""
      })
    }
  }

  deleteItem() {
    let filteredArray = this.state.todo.filter(x => x.done);
    console.log(this.state.todo);
    console.log(filteredArray);
    this.setState({
      "todo": filteredArray,
      "userInput": ""
    })

  }


  clearList() {
    let message = "Are you sure you want to delete all items?"
    let result = window.confirm(message);
    if (result) {
      this.setState({
        "todo": [
          { "item": "Walk The Dog", "done": false },
          { "item": "Drink more water", "done": false },
          { "item": "Brush teeth", "done": false }],
        "userInput": ""
      })
    }
  }

  render() {
    let formStyle = { width: "100%" }
    return (
      <div className="App">
        <div className="todobox">

          <h1>To-Do List App</h1>

          <div className="buttonContainer">
            <button className="btn btn-outline-primary btn-sm" type='button' onClick={this.addItem}>Add Item</button>
            <button className="btn btn-outline-warning btn-sm" type='button' onClick={this.deleteItem}>Delete Item</button>
            <button className="btn btn-outline-danger btn-sm" type='button' onClick={this.clearList}>Clear List</button>
          </div>

          <div className="itemlist">
            <form style={formStyle}>
              <input className="userInput" value={this.state.userInput} onChange={this.handleChange} placeholder=" Enter New Item" type="text"></input>
            </form>
            <Todolist todos={this.state.todo} toggleDone={this.toggleDone} listLength={this.state.todo.length} />
          </div>
        </div>
      </div >
    )
  }


}

export default ToDoItems;

That worked, duh! Thanks, @camperextraordinaire!

Thank you! I have not worked on the CSS part much, but relied on Bootstrap classes. Thought the form classes they have looked quite nice, but didn’t realize it causes functional workarounds to be made!

I’ll check out your recommendation and implement it. Thanks!