Mutating state directly is bad. Creating copy is too resource heavy. What to do?

Viewed 160

This is a small game that I am developing. Screenshot

Every one of these squares are represented by an object.

{
    row: i,
    col: j,
    isWall: false,
    isVisited: false,
    isPath: false,
    parentRow: null,
    parentCol: null,
    distance: Infinity
}

isWall property corelates to the black squares.

These are stored in a 2D array in the state.

this.state = {
    grid: getGrid()
}

I know we shouldn't mutate the state directly so every time I have to change a square from white to black, I copy the grid, change that square's isWall property to true and finally call setState.

getGridCopy = () => this.state.grid.map(row => row.map(square => ({...square})));

turnBlack = (row, col) => {
    const grid = this.getGridCopy();
    grid[row][col].isWall = true;
    this.setState({
        grid
    })
}

(The code is stripped down to only show relevant parts)

Now imagine I have to animate the whole grid from being white to completely black one square at a time. There are hundreds of squares in the grid and I have to change every one of them to black and to change just one square, I have to copy the whole 2D array of objects. This turns out to be very resource heavy that I can visibly see stutters in the animation. The animation is really smooth when I change the state directly without copying.

What do you suggest? I don't have much experience developing so any suggestions are welcome. This is the first project that is actually worth something. Can you suggest other ways of storing these objects instead of 2d arrays?

EDIT:

If there's just a single component with the whole grid as its state...

I have a Board component which has this state with grid 2d array. I mapped through the grid in board component's render method and render a Node component for each cell. Node component has no state. It receives properties as props, applies corresponding classNames to divs and renders them.

//Board component
render() {
    return(
        <div className="node-group">
        {
             grid.map((row, i) => (
                 <div key={i} className="node-row">
                 { row.map((node, j) => <Node {...node} key={j} ></Node> ) }
                 </div>
             ))
        }
        </div>
    )
}

//Node component
render() {
    let className = 'node';
    if (this.props.isWall) className += ' node-wall';
    return( <div className={className} ></div> )
}

Does this qualify as each cell being it's own react component?

2 Answers

No technique is bad or good on its own.

Having your state immutable allows any data-binding frameworks to detect change almost instantly, by comparing the references of old and new states, not having to go deep and do comparison prop-by-prop. Yet there's an obvious price to pay when state is updated.

Mutable state, on the other hand, is less computational-heavy, yet requires significant effort to detect the change.

The key question here is how your Views are organized. If there's just a single component with the whole grid as its state, you'll have to pay the price - either when updating the state or when trying to detect the changes.

However, if each Cell of your Grid is a separate View (React Component), it's enough to update just this object by replacing the corresponding element of your array with a new one. Something like this:

function handleGridUpdate(changedRow, changedCol, changes) {
  const newGrid = grid.map(
    (row, i) => rows.map(
      (cell, j) => i === changedRow && j === changedCol 
         ? { ...cell, changes }
         : cell
    )
  );
  setGrid(newGrid);
}

... then calling this function like this:

handleGridUpdate(row, col, { isWall: true } )

In this example, handleGridUpdate function takes the coords of changed grid element, as well as changes that should be applied. But the trick is, while list references are updated (as map returns a new copy of an array), their elements stay intact for each element that hasn't been changed. That saves a lot of time - both on copying and rerendering of the components.

Instead of using indexes, one can pass the original object instead (so each element of grid will be compared against it). But the important part is that only this object will be replaced with a new instance.

Check this article for more details. Even though it takes you through processing a List, and not a Grid, the key ideas demonstrated there can be applied in your case, too.

In most cases, where states are small, copying them entirely and then updating parts of it is not a problem.

However, if the state becomes bigger, or you have some deeply combined state, it can impact performance, and it becomes tedious to code. This is where persistence becomes important. Persistence ensures that immutable data structures keep references to parts of data that don't change, and gives back a copy of the object referencing the same 'old' data when possible, and new data when needed. This makes the structures both very memory efficient and performant.

There are libraries that take care of immutability and persistence for you. The most notable ones are:

  • Immutable.js: an immutable collections library that has persistence built in.

  • Immer: a smart way to update objects like you normally would, without the need to worry about modifying the original.

I myself am developing an immutable persistent collections library called Rimbu for TypeScript. It offers a plethora of immutable persistent collections that you can use in your state without being afraid to accidentally modify your data.

Since I liked you question, and need material to test my own library, I have created a small example in CodeSandbox of a basic game board using a Rimbu Table for the state.

Related