BeingSuman
BeingSuman

Reputation: 3323

Issue with Mutating State Array in React Component

Description:

I'm encountering an issue with updating a state array in a React component, specifically when dealing with 2D arrays. Despite following the recommended approach of immutably updating state using setState, I'm facing difficulties in ensuring that my state array is updated correctly.

Background:

I'm working on a React component that calculates the minimum cost path in a grid. The component maintains a state array called history, which stores the cumulative costs at each step of the calculation. The calculateMinimumCost function iterates through the grid, updating the history array with the cumulative costs for each step.

Problem:

While attempting to update the history array immutably using setState, I encountered unexpected behavior. Specifically, when attempting to update the history array with 2D arrays representing the cumulative costs, the state update does not reflect the expected changes. As a workaround, I've resorted to stringifying and parsing the 2D arrays before updating the state, which seems to resolve the issue, but I understand that this is not the recommended approach.

GitHub | Minimum Cost Path JS

Code Example:

// Example of problematic state update
setHistory([...history, [...minimumCostGenerator]]); // Does not update state as expected

// Workaround using stringify and parse
this.state.history.push(JSON.stringify([...newHistory])); // Works, but not recommended

setHistory = (newHistory) => {
  // this.setState({ history: newHistory }); // DOESN'T WORK
  this.state.history.push(JSON.stringify([...newHistory])); // WORKS
};

setMinimumCostGrid = (newMinimumCostGrid) => {
  // this.setState({ minimumCostGrid: newMinimumCostGrid }); // DOESN'T WORK
  this.setState({ minimumCostGrid: JSON.parse(newMinimumCostGrid) }); // WORKS
};

Question:

I'm seeking guidance on how to properly update a state array containing 2D arrays in React without resorting to workarounds like stringification and parsing. What could be causing the issue with the state update, and how can I ensure that the state array is updated correctly while adhering to React's best practices?

Any insights or suggestions would be greatly appreciated. Thank you!

Upvotes: 0

Views: 74

Answers (1)

Michael Harley
Michael Harley

Reputation: 1058

Hypothesis

Most likely your issue occurred because you use a lot of setState, in a loop. If I recall correctly, react 18 has a batched setState update, which could change the way the code is called. This is done to improve the UI re-rendering. (Batched setState Update ref: here)

Suggestion

  1. Change history to using ref instead, since you don't need this variable to re-render the UI
  2. Update your calculateMinimumCost function, to have a better reliable variable. (setState update is asynchronous and is unreliable when used in a quick succession)
  3. Change your setMinimumCostGrid (this function can also be removed and just update this.setState directly)

1. Change history to ref

class MinimumCostPath extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      // The grid can be put in a variable, outside of the class
      // If this grid won't ever be changed
      grid: [
        [1, 3, 1],
        [1, 5, 1],
        [4, 2, 1]
      ],
      minimumCostGrid: [],
      currentStep: 0
    };
    // Since history won't change at all, and you don't need to re-render UI for every history change, we will put this in a ref
    this.history = React.createRef([]);
  }

  ...
}

This is necessary to optimize your UI re-rendering

2. Update calculateMinimumCost function

Change your calculateMinimumCost function, you should only use setState at the end of your function.

  calculateMinimumCost = () => {
    const { grid } = this.state;
    const minimumCostGenerator = [];

    // Minimum Cost Path calculation algorithm
    for (let r = 0; r < grid.length; r++) {
      const row = [];
      minimumCostGenerator.push(row);

      for (let c = 0; c < grid[r].length; c++) {
        if(r === 0 && c === 0) {
          // Starting cell
          row.push(grid[r][c]);
        } else if(r === 0) {
          // 0th row
          row.push(grid[r][c] + minimumCostGenerator[r][c-1]);
        } else if(c === 0) {
          // 0th column
          row.push(grid[r][c] + minimumCostGenerator[r-1][c]);
        } else {
          // Other than 0 index cells
          row.push(Math.min(grid[r][c] + minimumCostGenerator[r-1][c], grid[r][c] + minimumCostGenerator[r][c-1]));
        }
      }
    }
    this.history.current = minimumCostGenerator;

    let step = 0;
    const totalSteps = grid.length * grid[0].length;

    const intervalId = setInterval(() => {
      if (step < totalSteps) {
        this.setMinimumCostGrid(this.history.current[step]);
        step++;
        this.setState({ currentStep: step });
      } else {
        clearInterval(intervalId);
      }
    }, 2000);
  };

3. Update setMinimumCostGrid function

  setMinimumCostGrid = (minimumCostGrid) => {
    this.setState({ minimumCostGrid });
  };

There is no need to make this immutable since you are not changing the value or anything.


I hope this can help to solve your issue.

More explanation about 2d array (if needed)

For your use case, there is no need to update the 2d array in a state. But if you still want to know about how to correctly create an immutable setState for 2d array, here is how you do it:

...
this.state = {
  grid: [
    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]
  ]
}
...

const newGrid = [ [ ... ], [ ... ] ]; // Same structure as `grid`
this.setState({ grid: newGrid.map((row, rIndex) => row[rIndex].map(col => col)) });
// The behavior above will create a completely immutable object for 2d array
// Correct

this.setState({ grid: [...newGrid] });
// The behavior above will create a partially immutable object (the inner array doesn't get immutable)
// Wrong

this.setState({ grid: JSON.parse(JSON.stringify(newGrid)) });
// The behavior above will create a new array, regardless of the depth (and much more easily)
// Correct

Upvotes: 0

Related Questions