javascriptreactjsstatesetstatearray.prototype.map

setState not updating array


I'm creating a todo app with reactjs and i'm trying to update my list of todo items when the user clicks on the input. When the user clicks I can change the item but the item does not get updated in state. I can't figure out why state is not updating. I think it has something to do with .map() and returning the todo object to the new array but I can't quite figure out what is going wrong. Any ideas?

App.js

import React from 'react'
import TodoItem from './components/TodoItem'
import TodosData from './TodosData'
import './App.scss'

class App extends React.Component {
  constructor() {
    super()

    this.state = {
      todoItems: TodosData
    }

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(id) {
    this.setState( prevState => {
      const updatedTodos = prevState.todoItems.map( todo => {
        if (todo.id === id) {
          todo.completed = !todo.completed
        }
        return todo
      })
      return {
        todoItems: updatedTodos
      }
    })
  }

  render() {
    const todosComponents = this.state.todoItems.map( item => {
      return <TodoItem key={item.id} todo={item} handleChange={this.handleChange}/>
    })
  
    return (
      <div className="TodoList">
        {todosComponents}
      </div>
    )
  }

}

export default App;

TodosItem.js

import React from 'react'

function TodoItem(props) {

    return (
        <div className="TodoItem">
            <input 
                type="checkbox" 
                checked={props.todo.completed} 
                onChange={ () => props.handleChange(props.todo.id) }
            />
            <p>{props.todo.text}</p>
        </div>
    )
}

export default TodoItem

Solution

  • Issue

    You are mutating your state objects. Since the nested object reference is the same React bails on rendering anything that may've updated. In other words, since the object reference is the same as the previous render React assumes nothing changed.

    handleChange(id) {
      this.setState( prevState => {
        const updatedTodos = prevState.todoItems.map(todo => {
          if (todo.id === id) {
            todo.completed = !todo.completed // <-- object mutation!!
          }
          return todo
        })
        return {
          todoItems: updatedTodos
        }
      })
    }
    

    Solution

    Along with shallow copying the array you also need to shallow copy any nested state that you are updating.

    handleChange(id) {
      this.setState( prevState => {
        const updatedTodos = prevState.todoItems.map(todo => {
          if (todo.id === id) {
            return {
              ...todo // <-- copy todo
              completed: !todo.completed, // <-- update propety
            }
          }
          return todo
        })
        return {
          todoItems: updatedTodos
        }
      })
    }