admin管理员组

文章数量:1434893

I have a wired behavior within my react app. I have tried to create a simple to-do list once I check the item I wanted to update the checkbox via the onChange method (handled by the parent App ponenet) which loops through the previous list of tasks and changes the one with the selected id. Once I do taskpleted = !taskpleted. I am getting this wired result:

pleted is true and then false?!!! I could not figure out what could be this.

{id: 2, text: "Wash my teeth", pleted: true}
pleted: false
id: 2
text: "Wash my teeth"
__proto__: Object

Here is my TodoItem ponent:

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

export default TodoItem;

And here is my App ponent:

class App extends React.Component {

  constructor(){
    super()
    this.state = {
      todos: tasks
    }
  }

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

  render(){
    console.log('render')
   
    const todoItems = this.state.todos.map(item =>  {
      return (
        <TodoItem 
          key={item.id} 
          task = {item} 
          handleChange={this.handleChange}
        />)
      });


      return (
        <div className="todo-list">
          {todoItems}
        </div>
      );
    
  }

}

I have a wired behavior within my react app. I have tried to create a simple to-do list once I check the item I wanted to update the checkbox via the onChange method (handled by the parent App ponenet) which loops through the previous list of tasks and changes the one with the selected id. Once I do task.pleted = !task.pleted. I am getting this wired result:

pleted is true and then false?!!! I could not figure out what could be this.

{id: 2, text: "Wash my teeth", pleted: true}
pleted: false
id: 2
text: "Wash my teeth"
__proto__: Object

Here is my TodoItem ponent:

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

export default TodoItem;

And here is my App ponent:

class App extends React.Component {

  constructor(){
    super()
    this.state = {
      todos: tasks
    }
  }

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

  render(){
    console.log('render')
   
    const todoItems = this.state.todos.map(item =>  {
      return (
        <TodoItem 
          key={item.id} 
          task = {item} 
          handleChange={this.handleChange}
        />)
      });


      return (
        <div className="todo-list">
          {todoItems}
        </div>
      );
    
  }

}

Share Improve this question asked Jul 3, 2021 at 20:40 DINA TAKLITDINA TAKLIT 8,43810 gold badges83 silver badges90 bronze badges 5
  • What do you mean by a wired result? – Shivam Commented Jul 3, 2021 at 20:43
  • {id: 2, text: "Wash my teeth", pleted: true} pleted: false id: 2 text: "Wash my teeth" __proto__: Object this one pleted should be true after a check not true then false I could not understand that – DINA TAKLIT Commented Jul 3, 2021 at 20:44
  • 2 Looks like you're mutating state. try if(task.id === id){return {...task, pleted: !task.pleted};} – pilchard Commented Jul 3, 2021 at 20:45
  • nested objects are passed as references, so when you directly change a property inside the map() you change it in the referenced object in the current state array as well. Instead, return a copy of the object with the new property value set. – pilchard Commented Jul 3, 2021 at 20:48
  • Wired in the tuto he done the same as I did and worked perfectly see the video here youtube./… t = 2.45.25. But let me try what u've mentioned. And Could you explain why is this working here? – DINA TAKLIT Commented Jul 3, 2021 at 20:51
Add a ment  | 

2 Answers 2

Reset to default 4

In JavaScript Objects are passed by reference not value, so if you directly mutate a property inside the map function it will change it in the original array prevState.todos. So instead you need to create a new copy of the object with the new value of the property.

      const updatedTodos = prevState.todos.map(task => {
        let newTask = {...task}
        if (task.id === id){
          newTask.pleted = !task.pleted
        }
        return newTask
      }) 

or

  const updatedTodos = prevState.todos.map(task => {
        if (task.id === id){
          return {...task, pleted: !task.pleted};
        }
        return task
      }) 

Objects are assigned and copied by reference. In other words, a variable stores not the “object value”, but a “reference” (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object itself.

See Object references and copying

Note: if you want only to see how the immutability is done! See the example of how it's done right in the The right way section (CTRL + F > The right way)! However the whole answer! Cover all the different lines! From immutability! And what if you go for mutation! And how and why the mutation (as bad it is) it's not working! And it can work!

Mutation and immutability

In react immutability is a best practice! And an important principle!

The best practice with setState() is to always use immutability! That what all do! And what the documentation remend!

Not only that but the documentation warn about you should never mutate the state directly this.state.something = coolAssignementVal; Never! And Never!

However we can still do it and it will works! Well you can try it!

Why! setState() => trigger a rerender always! Unless some update control life cycle hook or method control that! And control to not re-render!
(by using shouldComponentUpdate (class ponent)! Or React.memo in hooks (ref))

So if:

// mutating the data
this.state.msg = `New message ${counter}`;
counter++;
this.setState(this.state);

The ponent re-render ! Re-execute render()! And access this.state!

It can work! But in more plex app! it may cause problems and bugs! And cause things to not work! setState() => queue an update! Then a whole algorithm and machinery handle the queue of updates! A reference may not survive the whole thing!

Here a working codepen playground example to desmonstrate how this can work! (Totaly not advised though)! In some cases one can think about optimization! But you never know how the react internals will change in the future! In such situation! it's better to manage the thing out of the ponent state! (Your own plex class! Or whatever! And your responsability)! It gonna be more reusable that way! And strong!

In the playground example! you can see how i build a counter on the screen! Completly by directly mutating the state! Again a thing that we should not do!

Ok too much of talking! Let's get back to the problem!

  handleChange = (id) => {
    this.setState(prevState => {
      const updatedTodos = prevState.todos.map(task => {
        if (task.id === id){
          task.pleted = !task.pleted // direct mutation
          console.log(task)
        }
        return task
      }) // new array
      return {
        todos: updatedTodos 
      }
    })
  }

todos => a new array (map is immutable)! But the elements within map! Are passed by ref! So the tasks are the same objects instances as in prevState!

The code above doesn't work!

However this one does! Even though it's the same code! And passing by ref!

  handleChange = (id) => {
      const updatedTodos = Array.from(prevState.todos).map((task, i) => {
        if (task.id === id){
          console.log('id id id id id ===>')
          console.log(${task.pleted} => ! => ${!task.pleted})
          task.pleted = !task.pleted; // pletly just mutating
          console.log('task:')
          console.log(task)
        }

        console.log(task)

        return task;
      })

      console.log('change ===>')
      console.log(updatedTodos)
     });
     
    /**
    * using this version of set state (-no callback-)
    */
     this.setState({
        todos: updatedTodos
     }); // it works well like this
  }

Yup it works in this way!

And not when using the updated version of setState()! (setState((prevState) => newState))!

So why!? !! Yup we told you! setState() run asynchonously! And queue! And you don't know how the internal works! And we told you to use immutability! (The doc did!)!

Yup! But it still work if we don't use the updater version! if there was a problem it would have been the same in both !!??

So i have the answers! I went and checked the execution line by line! Down to the internal of React!

And the reason for this problem is that the updater callback (prevState) => newState is running twice in the dev Strict mode environment!

Don't use strict mode and it will works!

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

This gif can illustrate that!

In the bellow, We will be running in strict mode!

=>

In fact i get to see a wierd thing at first!

(For the code bellow! Just skim and look at the ments)

Check the part where the setTimeout() run twice !! ( i didn't get it at first)

handleChange = (id) => {
    console.log('handle change ==========++>');
    this.setState(prevState => {
      console.log('sest state ==========++>');
      console.log(Array.isArray(prevState.todos));
      console.log(prevState)
    
      this.updatedTodos = prevState.todos.map((task, i) => {
        if (task.id === id){
          console.log('id id id id id ===>' + i)
          console.log(`${task.pleted} => ! => ${!task.pleted}`)
          task.pleted = !task.pleted;
          task.___pleted = task.pleted; // added this to just see! It pass all right to the state! And in the later check in render! Meaning the state is passing all right!
          console.log('task:')
          console.log(task)
        }

        console.log('=>')
        console.log(task)

        return task;
      });

      console.log('Are refs the same ==')
      console.log(prevState.todos[1] === this.updatedTodos[1])

      console.log(prevState.todos[1])
      console.log(this.updatedTodos[1])

      console.log('change ===>')
      console.log(JSON.stringify(this.updatedTodos, null, 4)) // stringify so you get the instance representation at that exact point of time! (dev tool show the latest one)

      console.log(prevState.todos[1])
      console.log(this.updatedTodos[1])

      /*
       ******* here the kicker
       * The logging in the settimeout run twice! I didn't get it at first at all! Then after going down to the internal all became clear!
      */
      setTimeout(() => {
        console.log('FINAL STATE')
        console.log(prevState.todos[1])
        console.log(this.updatedTodos[1])
      }, 1000);     

      return {
        todos: this.updatedTodos
      }
    })
  }

You can see how it run twice!

setState()
You can see how it queue the update and state change!

First call ::

getUpdateState() execute!

And within it

The state update callback execute! A first time!

And the execution go as follow

And we have the first mutation! pleted => true

Then after this we hit the strict mode condtion:

Strict mode => run again!

We can see how the logs are disabled this time



And the unexpected Second call:

It's all due to this strict mode!

Strict mode run only in development! meaning in production this problem will not accure! And the example with mutation will still work! Even though again! Always use immutability!

We can see how prevState have the values from last mutation! (true)!


And here mutating again! Turning **true** =to=> **false**

That's how it's not working! All due to the second call! And how the prevState was mutated!

And here next the getUpdateState() is done!

Render() ! The end! Of this story!

The right way

If we do it right! Using immutability! Well why it does work!?

It still run twice! !!!

THE RIGHT WAY

handleChange = (id) => {
    console.log('handle change ==========++>');
    this.setState(prevState => {
      this.updatedTodos = prevState.todos.map((task, i) => {
        let pleted = task.pleted;
        if (task.id === id){
           pleted = !pleted;
        }

        return { 
          ...task, // immutable
          pleted
        };
      });

      return {
        todos: this.updatedTodos
      }
    })
  }

That works just perfectly! And as we should do! Always use immutability!

Why however this one works ?

In first call! The prev state ==> false

You can see how the prevState wasn't mutated and remain the same!

And how the new state was created!

Second call e and

preveState wasn't changed! Remained as in the first call!

That's the whole difference! So because of the immurtability, any number of calls will give always the same result! As the prevState remain the same!

Mutabilty screw that! And always the immutability is good at avoiding problems that mutability can cause! Immutability assure more robust code against bugs! Because it create isolation and separation!

And for the second run it just behaved as in the first call! Immutability assured the same prevState! And so the same result!

The mutation on the other hand! Screwed the prevState! And how in god sake you'll know that it will run twice! Not only that! But the logging is disabled on the second call!

That's a great example of how you should never mutate the state directly! And how you can't know what can go in the internals!

You must use immutability! If for any reason u need mutability for performance! Do it separately of the state (A separate class (logic ponent))! Mutate all you want! it's your own responsability! Only mit the mutated state once done to the state, by creating an immutable image of it! Or don't use the state for that data! Make an object and set a ref for it! Which you can access and use! And manage the whole yourself!

getStateFromUpdate() and the Strict mode and second unvisible call

Here the code for the method that is the root of the problem here!

Still ! The root of the problem is to not follow the documentation! And warning! You should always use immutability with the state!

That's how react design is! And work with!

function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
  switch (update.tag) {
    case ReplaceState:
      {
        var payload = update.payload;

        if (typeof payload === 'function') {
          // Updater function
          {
            enterDisallowedContextReadInDEV();
          }
          // first call to the updater method
          var nextState = payload.call(instance, prevState, nextProps);
          // state just right! (nextState.todos[1].disabled === true)

          {
            if ( workInProgress.mode & StrictMode) {
              // Strict mode [culprit] (Run only in Development! Production will not)
              disableLogs(); // that's why no logging in that second run!

              try {
                // running again (All evil happened here)
                payload.call(instance, prevState, nextProps);
              } finally {
                reenableLogs(); // logs activated again!
              }
            }

            exitDisallowedContextReadInDEV();
          }

          return nextState;
        } // State object


        return payload;
      }

    case CaptureUpdate:
      {
        workInProgress.flags = workInProgress.flags & ~ShouldCapture | DidCapture;
      }
    // Intentional fallthrough

    case UpdateState:
      {
        var _payload = update.payload;
        var partialState;

        if (typeof _payload === 'function') {
          // Updater function
          {
            enterDisallowedContextReadInDEV();
          }

          partialState = _payload.call(instance, prevState, nextProps);

          {
            if ( workInProgress.mode & StrictMode) {
              disableLogs();

              try {
                _payload.call(instance, prevState, nextProps);
              } finally {
                reenableLogs();
              }
            }

            exitDisallowedContextReadInDEV();
          }
        } else {
          // Partial state object
          partialState = _payload;
        }

        if (partialState === null || partialState === undefined) {
          // Null and undefined are treated as no-ops.
          return prevState;
        } // Merge the partial state and the previous state.


        return _assign({}, prevState, partialState);
      }

    case ForceUpdate:
      {
        hasForceUpdate = true;
        return prevState;
      }
  }

  return prevState;
}

To say, Immutability and mutablity from the doc

First to say it's always important to work with immutability when it es to state and react! That's the best practice! And what is advised and expected!

From the doc: https://reactjs/docs/react-ponent.html#setstate

NEVER mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

setState() will always trigger a re-render unless conditional rendering logic is implemented in shouldComponentUpdate(). If mutable objects are being used and the logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.

setState() => doesn't run immediatly => get queued => And a lot happen! Many state change are applied and grouped! There is a whole algorithm behind it!

To know better you can search for the keywords (React fibers and lanes)!

Curious about React fibers and internals and this advance topic! Here some picked links:

React fibers and lanes resources

1, 2, 3 (First pickup on google! All articles are good)

本文标签: javascriptReact changing records properties via map function does not work correctlyStack Overflow