0

I have an array of 16 objects which I declare as a state in the constructor:

this.state = {
      todos:[...Array(16)].map((_, idx) => {
              return {active: false, idx}
            }), 
}

Their status will get updated through an ajax call in ComponentDidMount.

componentDidMount()
{
    var newTodos = this.state.todos;
    axios.get('my/url.html')
        .then(function(res)
        {
          newTodos.map((t)=>{
            if (something something)
            {
                t.active = true;
            }
            else
            {
                t.active = false; 
            }
          }
          this.setState({
            todos:newTodos,
          })
        }
}

and then finally, I render it:

render(){
   let todos = this.state.todos.map(t =>{
     if(t.active === true){
         console.log('true'}
     else{
         console.log('false')
     }
   }) 
   return (
     <div></div>
   )
}

They all appear as active = false in the console, they never go into the if condition. When I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".

enter image description here

I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?

Tom
  • 2,545
  • 5
  • 31
  • 71
  • 2
    Can you align and fix your `componentWillMount` code? At a first look, it seems like the `this.setState` call is being called after the axios call, but the code you are sharing is neither aligned property, nor are all blocks closed so it's hard to say if the `setState` is being done from inside your `then` block or from inside the `componentWillMount` directly. If it's from the `then` you want to call it, you either have to use arrow functions or save the `this` context, if it's from the `componentWillMount` then you should `await` the axios call and make `componentWillMount` async – Icepickle Jan 16 '18 at 23:36
  • sorry. I edited it – Tom Jan 16 '18 at 23:41
  • 4
    then [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) returns a new array, it doesn't mutate in place. In the end your `newTodos` are still your old `this.state.todos`. And you still need to store the `this` context if you are not using arrow functions in the `then` callback (oh, and it's still not properly closed, the `then` function misses a `) `, but the alignment did the trick) – Icepickle Jan 16 '18 at 23:43
  • 3
    Never heard of `[...Array(16)].map`, cool trick. For those who don't know, `Array(16).map` doesn't actually iterate over any of the items because they are empty slots https://stackoverflow.com/questions/27433075/using-a-for-each-loop-on-an-empty-array-in-javascript – Ruan Mendes Jan 17 '18 at 00:39

2 Answers2

1

That is because you are actually not providing any new state, but mutating it instead.

React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:

var newTodos = this.state.todos; // newTodos === this.state.todos

this.setState({  todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything

The easiest solution, though probably not the most performant would be to clone your todos array:

var newTodos = [...this.state.todos]; // newTodos !== this.state.todos
Daniel Khoroshko
  • 2,623
  • 15
  • 26
  • 1
    He is not mutating anything the map returns a new array but it doesn't get assigned – Icepickle Jan 17 '18 at 00:37
  • 1
    because elements of the array are objects, and some of these objects are being mutated (property active is changed) – Daniel Khoroshko Jan 17 '18 at 00:48
  • 1
    and it's not only the map is not assinged to anything, the iterator of the map doesn't return anything either. but mutates the state – Daniel Khoroshko Jan 17 '18 at 00:50
  • 1
    the author simply mistook it for forEach – Daniel Khoroshko Jan 17 '18 at 00:51
  • 1
    True, I always find it confusing when people use a `map` instead of a `forEach`... I still don't think he will be able to do a lot with the response, but that might be just because his questions lacks data about what he wishes to do exactly (love those super secret to do apps ;)) – Icepickle Jan 17 '18 at 01:02
1
componentDidMount()
{
    var newTodos = [];    // <<<<<<<<<<<<<
    axios.get('my/url.html')
        .then(function(res)
        {
          newTodos = this.state.todos.map((t)=>{   //<<<<<<<<<<<<<<<
            if (something something)
            {
                t.active = true;
            }
            else
            {
                t.active = false; 
            }
            return t; //<<<<<<<<<<<<<<<<<<
          }     // <<<<< are you missing a semi-colon?
          this.setState({
            todos:newTodos,
          })
        }
}

The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:

xxx.map( t => ( "return t is implicit" ) );   
xxx.map( t => { "return t must be explicit" } );  

And, as @DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()

radarbob
  • 4,964
  • 2
  • 23
  • 36