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": ""
    }

One way would be to recreate the todo array and update the applicable object’s “done” property to true and then just use setState with the new modified todo. Below I assume you know the id of the item you need to update (I refer to it as idToUpdateDone.

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

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.

Sorry about that. I have updated my code to reflect this.state.todo. To help you further, it would be best to see your full code.

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.

@RandellDawson 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

props.todos.id will be undefined here. I think you meant id={x.id}.

Also, now that I can see your full code, you will need to change the following to:

updatedToDo.map(item => item.id == e.target.id

Why? Because the you want the id of the target of the click event and since the id will be a string, you would need to use the == operator instead of the === operator.

1 Like

Thank you, @RandellDawson ! 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;

You need this.toggleDone = this.toggleDone.bind(this) in your constructor.

1 Like

That worked, duh! Thanks, @RandellDawson!

I should have mentioned in my last reply. I had forgotten I had to add it to even have the toggleDone to fire.

I don’t know what your CSS looks like, but I think it would look better to not have the existing items as input elements. Why not just put them beside the checkboxes? If you do that, then you could modify your Todolist function to conditional render a strike-through line for any item where done is true.

function Todolist(props) {
  const strikeStyle = { textDecorationLine: 'line-through' };
  
  let newArray = props.todos.map(x =>
    <div className="input-group">
      <div className="input-group-prepend">
        <div className="input-group-text" style={x.done ? strikeStyle : null}>
          <input 
            type="checkbox"
            id={x.id}
            defaultChecked={x.done}
            onClick={props.toggleDone}
            aria-label="Checkbox for following text input"
            /> {x.item}
        </div>
      </div>
    </div>)
  return newArray;
}

And the in your toggleDone method, you would make a slight change to the Object.assign.

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

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!