Reputation: 3323
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.
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
Reputation: 1058
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)
ref
instead, since you don't need this variable to re-render the UIcalculateMinimumCost
function, to have a better reliable variable. (setState update is asynchronous and is unreliable when used in a quick succession)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.
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