Search code examples
javascriptreactjsstatecloning

React prevState changing when I change its clone, cloned with newArray = [...prevState.oldArray]


I am thoroughly confused and apologize ahead of time if this is a stupid question, I did my research and couldn't find the answer. I installed react following the official tutorial here. Then I followed the tutorial to the end with no problem. Everything worked right. I'm using Visual Studio Code in Ubuntu 20.04.02. Visual Studios Code reports: Version: 1.59.0 Commit: 379476f0e13988d90fab105c5c19e7abc8b1dea8 Date: 2021-08-04T23:13:20.182Z Electron: 13.1.7 Chrome: 91.0.4472.124 Node.js: 14.16.0 V8: 9.1.269.36-electron.0 OS: Linux x64 5.11.0-25-generic snap

Then I started following the following course: scrimba and confronted with an exercise I adopted the following solution to it:

    handleChange(id) {
            this.setState(prevState => {
            let newTodos = [...prevState.todos];
            const item = newTodos.find(item => item.id==id)
            if (newTodos == prevState.todos){
                console.log("Alarm, this should be true");
            }
            item.completed = !item.completed
            console.log('newTodos= ')
            console.log(newTodos)
            console.log('prevState.todos= ')
            console.log(prevState.todos)
            return {
                todos: newTodos
            }
        })
    }

The output I get from the console shows that preState was changed by me changing newTodos is this:

newTodos= 
App.js:38 (5) [{…}, {…}, {…}, {…}, {…}]0: {id: 1, text: "Take out the trash", completed: true}1: {id: 2, text: "Grocery shopping", completed: true}2: {id: 3, text: "Clean gecko tank", completed: false}3: {id: 4, text: "Mow lawn", completed: true}4: {id: 5, text: "Catch up on Arrested Development", completed: false}length: 5[[Prototype]]: Array(0)
App.js:39 prevState.todos= 
App.js:40 (5) [{…}, {…}, {…}, {…}, {…}]0: {id: 1, text: "Take out the trash", completed: true}1: {id: 2, text: "Grocery shopping", completed: true}2: {id: 3, text: "Clean gecko tank", completed: false}3: {id: 4, text: "Mow lawn", completed: true}4: {id: 5, text: "Catch up on Arrested Development", completed: false}length: 5[[Prototype]]: Array(0)

but now, if I use the second solution of the author of the course (his first solution also changed preState, but it was for a preditable reason) this doesn't happen any more. Here is his solution and the console output for the same change in the checkboxes (I check box corresponding with todos[1].id = 2)

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

and the console output, now is correct with preState not changing

(5) [{…}, {…}, {…}, {…}, {…}]0: {id: 1, text: "Take out the trash", completed: true}1: {id: 2, text: "Grocery shopping", completed: false}2: {id: 3, text: "Clean gecko tank", completed: false}3: {id: 4, text: "Mow lawn", completed: true}4: {id: 5, text: "Catch up on Arrested Development", completed: false}length: 5[[Prototype]]: Array(0)
App.js:59 (5) [{…}, {…}, {…}, {…}, {…}]0: {id: 1, text: "Take out the trash", completed: true}1: {id: 2, text: "Grocery shopping", completed: true}2: {id: 3, text: "Clean gecko tank", completed: false}3: {id: 4, text: "Mow lawn", completed: true}4: {id: 5, text: "Catch up on Arrested Development", completed: false}length: 5[[Prototype]]: Array(0)

Why is prevState changing in my solution? What am I missing?

Thanks for any help, I am afraid of having a major missunderstanding of either javascript or react.

I will include all the files required to reproduce this. index.js and index.css go in the src subdirectory of the react installation and the other files go in the src/components subdirectory.

Index.js file:

import React from 'react'
import ReactDOM from 'react-dom'

import App from './components/App'

import './index.css'

ReactDOM.render(<App />, document.getElementById('root'))

App.js file for the solution that works, the other commented out:

import React from "react"
 import TodoItem from "./TodoItem"
 import todosData from "./todosData"
 
 class App extends React.Component {
     constructor() {
         super()
         this.state = {
            todos: todosData
         }

         this.handleChange = this.handleChange.bind(this)
     }
     
     handleChange(id) {
        // My solution, which has the problem of changing the preState, no idea why
        //  this.setState(prevState => {
        //     let newTodos = [...prevState.todos];
        //     const item = newTodos.find(item => item.id==id)
        //     if (newTodos == prevState.todos){
        //         console.log("Alarm, this should be true");
        //     }
        //     item.completed = !item.completed
        //     console.log('newTodos= ')
        //     console.log(newTodos)
        //     console.log('prevState.todos= ')
        //     console.log(prevState.todos)
        //     return {
        //         todos: newTodos
        //     }
        // })

        /*****************  HIS SOLUTION***************** */

        this.setState(prevState => {
            const updatedTodos = prevState.todos.map(todo => {
                if (todo.id === id) {
                    return {
                        ...todo,
                        completed: !todo.completed
                    }
                }
                return todo
            })
            console.log(prevState.todos)
            console.log(updatedTodos)
            return {
                todos: updatedTodos
            }
        })
     }
     
     render() {
         const todoItems = this.state.todos.map(item => <TodoItem handleChange= {this.handleChange} key={item.id} item={item} />)
         
         return (
             <div className="todo-list">
                 {todoItems}
             </div>
         )    
     }
 }
 
 export default App

The child component TodoItem.js:

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

export default TodoItem

And the data used to run this:

const todosData = [
    {
        id: 1,
        text: "Take out the trash",
        completed: true
    },
    {
        id: 2,
        text: "Grocery shopping",
        completed: false
    },
    {
        id: 3,
        text: "Clean gecko tank",
        completed: false
    },
    {
        id: 4,
        text: "Mow lawn",
        completed: true
    },
    {
        id: 5,
        text: "Catch up on Arrested Development",
        completed: false
    }
]

export default todosData

Solution

  • I think you might have found the difference between two examples.

    [...prevState.todos]
    

    The problem is that items of the todos are Object like this.

    {
        id: 1,
        text: "Take out the trash",
        completed: true
    },
    

    Javascript passes object with reference (similar to pointer).

    const item = newTodos.find(item => item.id==id)
     if (newTodos == prevState.todos){
          console.log("Alarm, this should be true");
     }
     item.completed = !item.completed
    

    As item is a reference to an object here, chaging item.completed results in changing the prevState.

    Let's see the second example. As you can see, they made a new copy of todo in the map function

     const updatedTodos = prevState.todos.map(todo => {
            if (todo.id === id) {
                return {
                    ...todo,
                    completed: !todo.completed
                }
            }
            return todo
        })
    

    Here ...todo, completed: !todo.completed means use the field :value pair of todo and set the completed field value as !todo.completed