1
votes

I have a class component. I've added this line in the constructor:

this.handleChangedCells = this.handleChangedCells.bind(this);

I register a SocketIO listener in componentDidMount and call a function like so

socket.on("changedCells", this.handleChangedCells);

I am receiving the data as I console.log it once I receive it. The problem is that the function calls this.setState and it does not update the component. I have

handleChangedCells(data) {
    console.log(data);
    let new_board = this.state.board;
    data.map(arr => {
      arr.map(obj => {
        new_board[obj.ind[0]][obj.ind[1]] = {
          color: obj.color,
          alive: obj.alive
        };
      });
    });

    this.setState({ board: new_board });
}

The component that I have is as follows:

render() {
    return (
      <div className="boardContainer" style={container_style}>
        {this.state.board.map((row, r_ind) => {
          return (
            <div className="row" key={r_ind}>
              {row.map((col, c_ind) => {
                return (
                  <Cell
                    key={[r_ind, c_ind]}
                    cell_row_index={c_ind}
                    cell_col_index={r_ind}
                    socketProps={socket}
                    colorProps={this.state.color}
                    clickedColorProps={col.color}
                    aliveProps={col.alive}
                  />
                );
              })}
            </div>
          );
        })}
      </div>
    );
  }

As you can see, the component depends on the state which I update however, the component is not updated... Any idea of what is happening here?

EDIT: Here's a link to my project https://github.com/Dobermensch/ExpressTest.git The file in question is /client/components/Game.js

Edit 2: board is an array of arrays containing objects {color: [x,y,z], alive: boolean} and I am getting the changed cells, (dead cells) and marking them as dead in the board so ideally I would go the the dead cell's index and mark it as dead by board[row][col] = {color: [x,y,z], alive: false}

Edit 3: data structure of board is [[{}.{},{}],[{},{},{}]] and I am changing the properties of objects within the array

2
do not mutate the state. - Joseph D.
Do you get some error? - Manoj
am I mutating the state? and I get no errors - Ayudh
You are mutating state: let new_board = this.state.board; Now new_board === this.state.board so this.setState({ board: new_board }); would be the same as this.setState({ board: this.sate.board }); and would cause React to not detect any changes. We don't know what this.state.board is so can't really give you an answer as to how to set it. - HMR
In that case, since board is an array of arrays, how would I change one element in it and set the new "board" as state? I've tried let new_board = JSON.parse(JSON.stringify(this.state.board)); and then making the changes in new_board then setting new_board as state but still no luck - Ayudh

2 Answers

1
votes

I had faced similar issue while I had the combination of nested objects and array. I think setState doesn't handle nested updates very well.

Another problem is in your code. You are actually mutating the state.

let new_board = this.state.board; Here the board is an array. Javascript does not deep clone array and object. You are assigning the refererence of the board array to the new variable.

So, what you can do is just deep clone the board array.

let new_board = this.state.board.map(item => {...item}); // You may need to handle the multiple levels.

Also JSON.stringy and JSON.parse should work.

let new_board = JSON.parse(JSON.stringify(array));

Once, you have the deep clone of the state. You can modify the data and setState should trigger the rerender.

Update 1:

As you are using the state of the Cell to update it. The prop values are not updated on re-render.

So you should use getDerivedStateFromProps in your Cell component to make the prop data available to state.

static getDerivedStateFromProps(props, state) {
    return {
      r: props.clickedColorProps[0],
      g: props.clickedColorProps[1],
      b: props.clickedColorProps[2],
      alive: props.aliveProps
    };
  }

Note that the getDerivedStateFromProps will execute on every rerender of the component, even when the state change. So, you should have a condition inside the method to return the proper data.

0
votes

Please don't do a deep copy with JSON.parse, it is a really bad idea because you're creating new objects for everything and not only the things that change. If you know what pure components are then you know why JSON.parse is bad.

You can change only the things that need to change with the following code:

handleChangedCells(data) {
  console.log(data);
  let new_board = [...this.state.board];
  data.forEach(arr => {
    arr.forEach(obj => {
      //shallow copy only if it's not copied already
      if (
        new_board[obj.ind[0]] ===
        this.state.board[obj.ind[0]]
      ) {
        new_board[obj.ind[0]] = [
          ...new_board[obj.ind[0]],
        ];
      }
      new_board[obj.ind[0]][obj.ind[1]] = {
        color: obj.color,
        alive: obj.alive,
      };
    });
  });
  this.setState({ board: new_board });
}

If it still "doesn't work" then I suggest creating a sandbox demonstrating the problem. Instead of a real xhr you can just return changed sample data.

Demo of it working is here