Search code examples
reactjsreact-state-managementreact-state

Update field in state array object


I am building a simple TODO app in React. I have a todos array in the state, and each todo has a flag completed. When an item is clicked, I want to toggle the completed flag of that todo. Here's what I've done so far:

Todos.js

class Todos extends React.Component {
  constructor() {
    super();
    this.state = {
      todos: [
          { id:0, title:"Title 0", completed:false },
          { id:1, title:"Title 1", completed:true }
         ]
    };
    this.onTodoClicked = this.onTodoClicked.bind(this);
  }

  onTodoClicked(id) {
    console.log(id + " clicked");
    this.setState(prevState => {
      todos: prevState.todos.map((item, index) => {
        return index !== id ? item : { ...item, completed: !item.completed };
      });
    });
  }

  render() {
    return (
      <div>
        {this.state.todos.map((todoItem, key) => (
          <Todo id={key} key={key} todo={todoItem}
            todoClicked={this.onTodoClicked} />
        ))}
      </div>
    );
  }
}

Todo.js is just a simple component (yes, it could be functional as well)

class Todo extends React.Component {
  render() {
    return (
      <div onClick={() => this.props.todoClicked(this.props.id)}>
        {this.props.todo.title}
      </div>
    );
  }
}

In Todos.js the function onTodoClicked is called and it prints out the correct id, but nothing else changes in the state (I inspected the clicked object both with React dev tools and console.log's). The clicked item doesn't change its completed flag. What am I doing wrong?


Solution

  • That's how it should look:

    class Todos extends React.Component {
      constructor() {
        super();
        this.state = {
          todos: [
              { id:0, title:"Title 0", completed:false },
              { id:1, title:"Title 1", completed:true }
             ]
        };
        this.onTodoClicked = this.onTodoClicked.bind(this);
      }
    
      onTodoClicked(id) {
        console.log(id + " clicked");
        this.setState(prevState => ({
          todos: prevState.todos.map((item, index) => {
            return item.id !== id ? item : { ...item, completed: !item.completed };
          })
        }));
      }
    
      render() {
        return (
          <div>
            {this.state.todos.map((todoItem, key) => (
              <Todo key={key} todo={todoItem}
                todoClicked={this.onTodoClicked} />
            ))}
          </div>
        );
      }
    }
    
    class Todo extends React.Component {
      render() {
        return (
          <div onClick={() => this.props.todoClicked(this.props.todo.id)}>
            {this.props.todo.title} {this.props.todo.completed && "completed"}
          </div>
        );
      }
    }
    
    ReactDOM.render(<Todos/>, document.getElementById("root"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    <div id="root"></div>