Delayed redux dispatch call speeds up UI update - javascript

I'm trying to create a simple block stacking game in ReactJs, Redux and TS. I want to move the top line in the stack from left to right and back, infinitely, until Space or Enter key is hit. First when the UI loads it works fine but soon after it starts to speed up the UI updates with inconsistent updates. Like this...
Game Preview
Inconsistent UI updates log
I want to update the position by 1 step every render after 200ms of delay.
I'm trying to do it like this.
useEffect(() => {
if (play) {
setTimeout(() => {
moveStackLine(JSON.parse(JSON.stringify(data)), direction);
}, 200);
}
});
moveStackLine function:
const moveStackLine = useCallback((_data: GameData, directionCopy: 'left' | 'right') => {
console.log('_data1', _data);
let _direction = directionCopy;
const lastIndexData = _data[_data.length - 1];
if (_direction === 'right' && lastIndexData[1] < gameWidth) {
lastIndexData[0] += 1;
lastIndexData[1] += 1;
}
if (_direction === 'left' && lastIndexData[0] > 0) {
lastIndexData[0] -= 1;
lastIndexData[1] -= 1;
}
if (lastIndexData[1] === gameWidth - 1) {
_direction = 'left';
} else if (lastIndexData[0] === 0) {
_direction = 'right';
}
_data.splice(_data.length - 1, 1, lastIndexData);
dispatch({ type: ACTIONS.CHANGE_ACTIVE_LINE_START_END_POS, payload: _data });
if (_direction !== directionCopy) {
dispatch({ type: ACTIONS.CHANGE_DIRECTION, payload: _direction });
}
}, [dispatch, gameWidth]);
Above function changes the start and end position of the data Array inside my rootState.
initialState:
export const gameState: InitialGameState = {
currentLine: 0,
gameWidth: 16,
gameHeight: 20,
direction: 'right',
play: true,
data: [[2, 5], [0, 3], [4, 7]],
}
Each array inside data array represents a line and the start and end position of the line.
My render logic looks like this.
const renderLine = (startEndIndex: Array<number>) => {
const columns = [];
for (let j = 0; j < gameWidth; j++) {
columns.push(
<div className={cx(styles.ball, { [styles.active]: j >= startEndIndex[0] && j <= startEndIndex[1] })}></div>
);
}
return columns;
}
const renderGame = () => {
const lines = [];
for (let i = 0; i < gameHeight; i++) {
lines.push(
<div id={i.toString()} className={styles.line}>
{renderLine(data[i] || [-1, -1])}
</div>
)
}
return lines;
}
return (
<>
<div className={styles.gameContainer}>
{renderGame()}
</div>
</>
)

You are directly modifying _data here, which I assume is also the object that you retrieved from the Redux store. Which would mean you are modifying the redux store outside of a reducer.
Things like this would cause all kinds of side effects.
And not only are you doing this, you are also modifying lastIndexData - which is also a reference to an object in the store - here, again, you are modifying the store directly instead of updating it with a reducer.
Try
const moveStackLine = useCallback((data: GameData, directionCopy: 'left' | 'right') => {
// .concat will create a new array reference that you can (flatly) modify as you want
const _data = data.concat()
// .concat will create a new array reference that you can (flatly) modify as you want
lastIndexData = _data[_data.length - 1].concat();

Related

Cell Component not re-rendering on Board in React

Edit
I was able to start getting the cells to rerender, but only after adding setCellsSelected on line 106. Not sure why this is working now, react is confusing.
Summary
Currently I am trying to create a visualization of depth first search in React. The search itself is working but the cell components are not re-rendering to show that they have been searched
It starts at the cell in the top left and checks cells to the right or down. Once a cell is searched, it should turn green. My cells array state is changing at the board level, so I assumed that the board would re-render but to no avail. For now I am only searching the cells straight below (0,0) as a test.
Code
Board.js
const Cell = (props) => {
let cellStyle; // changes based on props.value
if (props.value === 3) cellStyle = "cell found";
else if (props.value === 2) cellStyle = "cell searched";
else if (props.value === 1) cellStyle = "cell selected";
else cellStyle = "cell";
return <div className={cellStyle} onClick={() => props.selectCell()}></div>;
};
const Board = () => {
// 0 = not searched
// 1 = selected
// 2 = searched
// 3 = selected found
const [cells, setCells] = useState([
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
new Array(10).fill(0),
]); // array of cells that we will search through, based on xy coordinates
const [start, setStart] = useState("00");
const selectCell = (x, y) => {
// Make sure to copy current arrays and add to it
let copyCells = [...cells];
copyCells[x][y] = 1;
setCells(copyCells);
};
const renderCell = (x, y) => {
return (
<Cell
key={`${x}${y}`}
value={cells[x][y]}
selectCell={() => selectCell(x, y)}
/>
);
};
const renderBoard = () => {
let board = [];
for (let i = 0; i < cells.length; i++) {
let row = [];
for (let j = 0; j < cells.length; j++) {
let cell = renderCell(i, j);
row.push(cell);
}
let newRow = (
<div className="row" key={i}>
{row}
</div>
);
board.push(newRow);
}
return board;
};
const startSearch = () => {
// start with our current startingCell
const startX = parseInt(start[0]);
const startY = parseInt(start[1]);
let copyCells = [...cells];
const searchCell = (x, y) => {
console.log("Coordinate:", x, y);
if (x >= cells.length) return;
if (y >= cells.length) return;
let currentCell = copyCells[x][y];
console.log(copyCells);
if (currentCell === 1) {
copyCells[x][y] = 3;
console.log("Found!");
console.log(x, y);
return;
} else {
console.log("Not Found");
copyCells[x][y] = 2;
setTimeout(() => {
searchCell(x + 1, y);
}, 3000);
setTimeout(searchCell(x, y + 1), 3000);
}
setCells(copyCells);
setCellsSelected(['12']) // this works for some reason
};
searchCell(startX, startY);
};
return (
<>
<div style={{ margin: "25px auto", width: "fit-content" }}>
<h3>Change search algorithm here!</h3>
<button onClick={() => startSearch()}>Start</button>
</div>
<div className="board">{renderBoard()}</div>
</>
);
};
export default Board;
You have two issues:
working demo: https://codesandbox.io/s/wonderful-cartwright-qldiw
The first issue is that you are doing setStates inside a long running function. Try instead keeping a search state and update the location instead of calling recursively.
The other issue is at the selectCell function
you are copying the rows by reference(let copyCells = [...cells];), so when you change the cell (copyCells[x][y] = 1;) you are also changing the original row, so the diffing will say the state did not change.
const selectCell = (x, y) => {
// Make sure to copy current arrays and add to it
let copyCells = [...cells];
copyCells[x][y] = 1;
setCells(copyCells);
};
try changing to let copyCells = [...cells.map(row=>[...row])];

i have issues regarding react state

Hi i have been building a sorting algorithms visualization it works so far but i have a doubt regarding state object.
consider the below code:
import React,{Component} from 'react';
import getMergeSortAnimations from './Person/Person';
import bubbleSortAnimations from './Person/BubbleSort';
import './App.css';
class App extends Component{
state = {
array: [],
bar_width:2
};
componentDidMount() {
this.generateArray();
}
generateArray = ()=> {
const array = [];
let val = document.querySelector('#size').value;
if(val<=10)
{
this.setState({bar_width:8});
}
else if(val<=20 && val>10)
this.setState({bar_width:7});
else if(val<=50 && val>20)
this.setState({bar_width:6});
else if(val<=100 && val>50)
this.setState({bar_width:5});
else if(val<=150 && val>100)
this.setState({bar_width:3});
else
this.setState({bar_width:2});
for (let i = 0; i < val; i++) {
array.push(this.randomIntFromInterval(5, 450));
}
this.setState({array});
}
randomIntFromInterval = (min, max)=> {
return Math.floor(Math.random() * (max - min + 1) + min);
}
mergeSort = ()=>{
let t ;
console.log(this.state);
const animations = getMergeSortAnimations(this.state.array);
console.log(this.state);
for (let i = 0; i < animations.length; i++) {
const arrayBars = document.getElementsByClassName('element');
const isColorChange = i % 3 !== 2;
if (isColorChange) {
const [barOneIdx, barTwoIdx] = animations[i];
const barOneStyle = arrayBars[barOneIdx].style;
const barTwoStyle = arrayBars[barTwoIdx].style;
const color = i % 3 === 0 ? 'red' : '#007bff';
setTimeout(() => {
barOneStyle.backgroundColor = color;
barTwoStyle.backgroundColor = color;
}, i*10);
} else {
setTimeout(() => {
const [barOneIdx, newHeight] = animations[i];
const barOneStyle = arrayBars[barOneIdx].style;
barOneStyle.height = `${newHeight}px`;
}, i*10);
}
}
}
render() {
return (
<div>
<header>
<input className="slider" onChange={this.generateArray} type="range" min="5" max="200"
id='size'/>
<nav>
<ul>
<li><button onClick={this.generateArray} id="new" >New array</button></li>
<li><button onClick={this.mergeSort} id="mergesort" >Merge Sort</button></li>
<li><button onClick={this.bubbleSort} id="bubbleSort" >Bubble sort</button></li>
</ul>
</nav>
</header>
<div className="container">
<br></br>
{this.state.array.map((value, idx) => (
<div
className="element"
key={idx}
style={{
width:`${this.state.bar_width}px`,
height:`${value}px`
}}></div>
))}
</div>
</div>
);
}
}
Merge sort Code:
export default function getMergeSortAnimations(array) {
const animations = [];
mergeSort(array, 0, array.length - 1,animations);
return animations;
}
function mergeSort(array,low, high,animations) {
if(low<high)
{
const mid = Math.floor((low + high) / 2);
mergeSort(array, low, mid,animations);
mergeSort(array, mid + 1, high,animations);
merge(array, low, high,animations);
}
}
function merge(array,low,high,animations) {
let a = [];
let k = low;
let i = low;
let mid = Math.floor((low+high)/2);
let j = mid + 1;
while (i <= mid && j <= high) {
animations.push([i, j]);
animations.push([i, j]);
if (array[i] <= array[j]) {
animations.push([k, array[i]]);
a[k++] = array[i++];
} else {
animations.push([k, array[j]]);
a[k++] = array[j++];
}
}
while (i <= mid) {
animations.push([i, i]);
animations.push([i, i]);
animations.push([k, array[i]]);
a[k++] = array[i++];
}
while (j <= high) {
animations.push([j, j]);
animations.push([j, j]);
animations.push([k, array[j]]);
a[k++] = array[j++];
}
for(let o=low;o<k;o++)
{
array[o] = a[o];
}
}
The merge sort function is under src->Person->Person as mentioned in the import section,it just returns animations array which i use for visualization.
Now the generateArray function generates an array and sets it to state using setSate() Method.When this is done the user can select mergesort and the code runs.But as you can see getMergesortAnimations() returns the animations array after the actual mergesort happens.But the question is:
"""When i console the state array before calling getMergesortAnimations() it displays a sorted array.It happens even before the mergesort is called and how is the state set to the sorted array without acutally using setState method?"""
This is very confusing to me ....
Thanks.
// From Generate random number between two numbers in JavaScript
export default App;
From a quick look at your code I can see that at the very end of your mergeSort function you are doing:
for(let o=low;o<k;o++)
{
array[o] = a[o];
}
Which is modifying the array in place. So after you in your component call:
const animations = getMergeSortAnimations(this.state.array);
this.state.array will be modified in place. This is something you should not do in React, from React docs:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
In order to fix this just fix the last couple lines of your mergeSort function so that it does not assign to array but rather creates a new array and returns that.

Structure order of an array based on an item in the array has certain value set to true

I have incoming data from my CMS that all contains the value featuredProject, it can be true, null or false. If it's true I add a css class that makes it span across the screen. To help keep my grid structure intact I need to always have at least two projects with featuredProject set to false or null before or after a project with featuredProject set to true.
The problem is that the data from the CMS doesn't respect the supposed design of the grid and can and probably will come out distorted from what I need to loop it out correctly.
What I've been trying to achive now is a function that loops over all projects in the array from the CMS and looks at what the featuredProject value is on the current project in the loop. If it's set to true I look back at the past 2 indexes of the array to see what their featuredProject value is. If both of them doesn't have the value set to false or null I want to sort the array by shifting the current index one step forward, exit the loop and then loop over it again to check if all the values are in order.
Right now I get an error where one value of the array is undefined which I don't really get why.
An image displaying the grid I would like to achieve.
https://imgur.com/a/KrdwlNI
The code I have right now.
The function to move an index
function move(array: any[], from: number, to: number) {
if (to === from) return array
const target = array[from]
const increment = to < from ? -1 : 1
for (let k = from; k != to; k += increment) {
array[k] = array[k + increment]
}
array[to] = target
return array
}
The function that checks the value of featuredProject
const sortImageGrid = (arr: any[]): any[] => {
// console.log(arr)
const sortedArr: any[] = []
arr.map((item, index) => {
if (item === undefined) {
return
}
// console.log(item)
if (index === 0) {
sortedArr.push(item)
} else if (index === 1 && item.featuredProject) {
move(arr, index, index + 1)
} else if (index === 1 && !item.featuredProject) {
sortedArr.push(item)
} else if (
item.featuredProject &&
!arr[index - 1].featuredProject &&
!arr[index - 2].featuredProject
) {
sortedArr.push(item)
} else if (
(item.featuredProject && arr[index - 1].featuredProject === true) ||
(item.featuredProject && arr[index - 2].featuredProject === true)
) {
move(arr, index, index + 1)
} else {
sortedArr.push(item)
}
console.log(sortedArr)
})
if (sortedArr.length === arr.length) {
return sortedArr
} else {
return []
}
}
How I run the function in the component where I map out the projects
const [gridSorted, setGridSorted] = useState(false)
let sortedArr: any[] = []
if (data.allSanityProject.nodes && data.allSanityProject.nodes.length > 0) {
while (gridSorted === false) {
sortedArr = sortImageGrid(data.allSanityProject.nodes)
// console.log(sortedArr)
if (sortedArr.length === data.allSanityProject.nodes.length) {
setGridSorted(true)
return
}
}
}
One way I'm thinking you could do this is filter out the responses into separate arrays:
let falsyValues = [];
let truthyValues = []
for(let item of arr){
if(item.featuredProject){
truthyValues.push(item)
} else {
falsyValues.push(item)
}
}
Then create a function that "zips" them based on your criteria:
function zip(falsyArr, truthyArr){
let zippedValues = []
for(let i = 0, n = 0; i < falsyArr.length; i++, n += 2){
if(!falsyArr[n] || !falsyArr[n+1]){
return zippedValues
}
zippedValues.push(falsyArr[n], falsyArry[n+1], truthyArr[i])
}
return zippedValues;
}
You'll just have to slightly adjust the zip function to account for arrays with differing lengths and determine which one you want to loop over (probably the max length between the two).

Why does onClick cause a loop?

I'm trying to implement Minesweeper in React and whenever the player clicks on a mine, the board is reset and re-rendered, but the cell that the player initially clicked containing the mine appears to fire onClick again after the board resets.
I've noticed additionally that if I don't reset the board after hitting a mine, but instead call alert() and then return without changing state, then game loops until a stack overflow occurs.
This is how my stateful board component looks when I display an alert after game over and do not change state:
render() {
let squareGrid = this.state.currentGrid.slice();
return (
squareGrid.map((row, y) => { //For each row
return ( //Create a division
<div key={y}>
{
row.map((state, x) => {//Render a square for each index
let value = (state.touched) ? state.minedNeighbors :"_";
return <Square mine={squareGrid[y][x].mine} key={x} disabled={state.touched} val={value}
onClick={() => this.handleClick(y, x)}> </Square>
})}
</div>
)
}
)
)
}
handleClick(row, column) {
// Get copy of grid
const grid = this.state.currentGrid.slice();
//If the player clicks a mine, game over.
if (grid[row][column].mine) {
//this.resetGame(); //This function does cause a state change
alert("You have died.");
return;
}
//Non-pure function that mutates grid
this.revealNeighbors(row, column, grid);
this.setState({
currentGrid: grid
})
}
My Square component is a function
function Square(props) {
return (
<button className={"gameButton"} disabled={props.disabled} onClick={props.onClick}>
{props.val}
</button>
);
}
The code, as is, will repeatedly display an alert over and over again once the player clicks a mine.
If I uncomment the line in handleClick that resets the game, the board will be correctly reset, but the cell that the player last clicked will be revealed as if the player had clicked it again after the board reset.
A lot of the other posts that have had my issue are due to the onClick attribute containing a function call instead of a function pointer, but as far as I can tell, I'm not calling the function directly in render; I'm providing a closure.
Edit:
Here is the full code for my Board component.
class Board extends React.Component {
constructor(props) {
super(props);
let grid = this.createGrid(_size);
this.state = {
size: _size,
currentGrid: grid,
reset: false
}
}
createGrid(size) {
const grid = Array(size).fill(null);
//Fill grid with cell objects
for (let row = 0; row < size; row++) {
grid[row] = Array(size).fill(null);
for (let column = 0; column < size; column++) {
grid[row][column] = {touched: false, mine: Math.random() < 0.2}
}
}
//Reiterate to determine how many mineNeighbors each cell has
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
grid[r][c].minedNeighbors = this.countMineNeighbors(r, c, grid)
}
}
return grid;
}
handleClick(row, column) {
const grid = this.state.currentGrid.slice();
//If the player clicks a mine, game over.
if (grid[row][column].mine) {
//this.resetGame();
//grid[row][column].touched = true;
alert("You have died.");
return;
}
//Non-pure function that mutates grid
this.revealNeighbors(row, column, grid);
this.setState({
currentGrid: grid
})
}
//Ensure cell is in bounds
checkBoundary(row, column) {
return ([row, column].every(x => 0 <= x && x < this.state.size));
}
revealNeighbors(row, column, grid) {
//Return if out of bounds or already touched
if (!this.checkBoundary(row, column) || grid[row][column].touched) {
return;
}
//Touch cell
grid[row][column].touched = true;
if (grid[row][column].minedNeighbors === 0) {
//For each possible neighbor, recurse.
[[1, 0], [-1, 0], [0, 1], [0, -1]]
.forEach(pos => this.revealNeighbors(row + pos[0], column + pos[1], grid));
}
}
countMineNeighbors(row, column, grid) {
let size = grid.length;
//Returns a coordinate pair representing the position of the cell in the direction of the angle, eg, Pi/4 radians -> [1,1]
let angleToCell = (angle) => [Math.sin, Math.cos]
.map(func => Math.round(func(angle)))
.map((val, ind) => val + [row, column][ind]);
return Array(8)
.fill(0)
.map((_, ind) => ind * Math.PI / 4) //Populate array with angles toward each neighbor
.map(angleToCell)
.filter(pos => pos.every(x => 0 <= x && x < size))//Remove out of bounds cells
.filter(pos => grid[pos[0]][pos[1]].mine)//Remove cells that aren't mines
.length //Return the length of the array as the count
}
resetGame() {
this.setState({
currentGrid: this.createGrid(this.state.size)
}
)
}
render() {
let squareGrid = this.state.currentGrid.slice();
return (
squareGrid.map((row, y) => { //For each rows
return ( //Create a division
<div key={y}>
{
row.map((state, x) => {//Render a square for each index
let value = (state.touched) ? state.minedNeighbors : "_";
return <Square mine={squareGrid[y][x].mine} key={x} disabled={state.touched} val={value}
onClick={() => this.handleClick(y, x)}/>
})}
</div>
)
}
)
)
}
}
All the time when you change your state your render method is called.
this.setState({
currentGrid: grid
})
probably you should implement a method called shouldComponentUpdate to prevent this to happen. Also, your slice is not being resolved. I'd suggest you to try with async/await.
I've had a problem like this if you click on a element that becomes rerendered. I'm not sure if this will solve the problem in your particular situation but I've found 2 solutions that have worked for me in the past.
One is to put a flag in your mouse click event,
if(!mouseDownFlag){
mouseDownFlag = true;
//the rest of your onetime code
}
and then have the flag removed on the mouseupevent
Alternatively, sometimes using the mousedown event instead of mouseclick, can be more predictable.
Hopefully one of these solutions help.
You need to change the way you pass and use the click function (when passing a function as a prop you only want to pass a reference to the function not call it, hence excluding the ())
return
<Square
mine={squareGrid[y][x].mine}
key={x} disabled={state.touched}
val={value}
// **** change line below
onClick={this.handleClick}
> </Square>
And when you call it
<button
className={"gameButton"}
disabled={props.disabled}
// **** change line below
onClick={() => props.onClick()}>
{props.val}
</button>

I need my array to return and array back to another variable while also including its previous array members, no idea how to go about it

This is the test code that it's supposed to pass
function makeArray() {
const array = [];
const t = 10;
for (let i = 0; i < t; i++) {
array.push("I am a strange loop.");
}
return [array, t];
}
describe('loops', () => {
jsdom({
src: fs.readFileSync(path.resolve(__dirname, '..', 'loops.js'), 'utf-8'),
});
describe('forLoop(array)', () => {
it('adds `"I am ${i} strange loop${i === 0 ? \'\' : \'s\'}."` to an array 25 times', () => {
const [array, t] = makeArray();
const strangeArray = forLoop(array);
const testArray = strangeArray.slice(array.length);
const first = "I am 1 strange loop.";
const rest = "I am 24 strange loops.";
expect(strangeArray[11]).to.equal(first);
expect(strangeArray[34]).to.equal(rest);
expect(strangeArray.length).to.equal(t + 25);
});
});
});
this is my code to return the function to strangeArray what I am thinking is that 35 is the total number of members in the array and as the test pass requires me to have 'expect(strangeArray[11]).to.equal(first)' 11th value to be equal to my function return as
"I am 1 strange loop."
function forLoop(array) {
for (let i = 0; i < 35; i++) {
if (array[i] === "I am a strange loop.") {
return;
}
else {
array.push("I am ${i} strange loops.");
}
}
return [array,i];
}
Not sure what you mean exactly but I guess you just want the test to pass? The problem is that the first loop has 'loop' as singular and your indexes don't work either since they would start at 11. That's why your code doesn't work. You can just push to the original array.
function forLoop(array){
for(let i = 0; i < 25; i++){
array.push(`I am ${i} strange loop${i > 1 ? '' : 's'}.`)
}
return array
}

Categories