Search code examples
javascriptreactjsreact-hookscomponents

Why does my component not auto update after a state change?


I am following a tutorial on Scrimba for React and currently doing the Boxes challenge part 4.

I pretty much have a function that takes in an ID, and that ID relates to an array of key-value pairs.

const [squares, setSquares] = React.useState(boxes)

function toggle(id) {

        /**
         * Challenge: use setSquares to update the
         * correct square in the array.
         * 
         * Make sure not to directly modify state!
         * 
         * Hint: look back at the lesson on updating arrays
         * in state if you need a reminder on how to do this
         */

        setSquares(oldSquares => {
            let newSquares = oldSquares
            newSquares[id - 1].on = !oldSquares[id - 1].on
            return newSquares
        })
    }

Here is what squares contains:

[{id: 1, on: true}, {id: 2, on: false}, {id: 3, on: true}, {id: 4, on: true}, {id: 5, on: false}, {id: 6, on: false}]

My function above properly inverts the value for the on key and I prove this through print statements, however my component displaying the squares does not update even though the state is updating.

Anyone know why?


Solution

  • You're not creating a new array in state, but mutating the existing one. So the framework doesn't know there's a state update.

    This creates a reference to the same array in memory:

    let newSquares = oldSquares
    

    This mutates a value in that array:

    newSquares[id - 1].on = !oldSquares[id - 1].on
    

    And this returns to state the same reference to the same array:

    return newSquares
    

    Don't mutate state. Instead, create a new array with the updated elements:

    setSquares(oldSquares => 
      oldSquares.map((s, i) =>
        i === id - 1 ?
          { ...s, on: !s.on } :
          s
      )
    )
    

    In this case .map() returns a new array reference, and within the .map() callback we conditionally return a new object with the on property negated if the index equals id - 1 or return the object as-is for all other indices.


    For a much more verbose version to perhaps better illustrate the logic, it's effectively the same result as this:

    setSquares(oldSquares => {
      let newSquares = oldSquares.map((s, i) => {
        if (i === id - 1) {
          return { ...s, on: !s.on };
        } else {
          return s;
        }
      });
      return newSquares;
    })