Why does onClick cause a loop? - javascript

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>

Related

Map an array, but place X amount of items each pass

I'm creating a grid of client logos using Tailwind, react, and GSAP. Client logo paths are loaded in via a json file. The client logo list is pretty long, so I thought it would be interesting to have the images in each grid col-spans ('cells') fade to a different image every few seconds.
My solution thus far is to map through all the logos and stack a certain number of them on top of each other as absolute before moving onto the next col span and then animate them in and out using the ids of the col-spans. I'm having trouble wrapping my mind around the approach. Especially with responsibly changing the grid-cols.
The approach so far (some pseudo-code some irl code):
const maxCols = 4
const maxRows = 3
const itemsPerRow = Math.floor( logosList.length()/maxCols/maxRows)
const isExtra = () =>{
if(logosList.length() % itemsPerRow >0) return true
else return false
}
const numRows = Math.floor( logosList.length()/maxCols )
export default function ClientImages(){
useEffect(() => {
for(i = 0; i <= i/maxCols/maxRows; i++ )
// to be figured out gsap method
gsap.to(`img-${i}`, {animate stuff})
},);
function setLogos(){
let subset
for ( index = 0; index == maxCols * maxRows; index++ ){
if(isExtra){
subset = logosList.slice(index, itemsPerRow + 1)
}
else subset = logosList.slice(index, itemsPerRow)
return(
<div className="col-span-1 relative" id={`clientColSpan-${index}`}>
{subset.map((logo) => {
<Image className='absolute' src={logo.src} yada yada yada />
})}
</div>
)
}
}
return(
<div className="grid grid-cols-2 md:grid-cols-4 2xl:grid-cols-6">
{setLogos}
</div>
)
}
Here's a visual representation of my thought process
Here's my solution based on your visual representation.
Create the grid
As we need two different grids for desktop and mobile, it's better to have a function that does this job specifically.
// spread images to grid
const createGrid = (images, col, row) => {
const totalItems = images.length;
const totalCells = col * row;
const itemsInCell = Math.floor(totalItems / totalCells);
let moduloItems = totalItems % totalCells;
// create grid
const grid = [];
for (let i = 0; i < totalCells; i++) {
let cell = [];
for (let j = 0; j < itemsInCell; j++) {
const index = i * itemsInCell + j;
cell.push(images[index]);
}
grid.push(cell);
}
// handle modulo items
while (moduloItems > 0) {
grid[totalCells - 1].push(images[totalItems - moduloItems]);
moduloItems--;
}
return grid;
};
const images = [1,2,3,4,...,37];
cosnt grid = createGrid(images, 2, 3); // [ [1,2,3,4,5,6], [7,8,9,10,11,12], ... [] ]
The createGrid() will return an array of grid cells, each cell will contain a number of items that meet your expectation. With this grid cells array, you have enough data to create your HTML.
Handle the responsive grid
With the provided grid array we can create responsive HTML grid layouts based on the window's width.
const createHTMLFromGrid = gridArray =>{// your function for HTML};
let html =
window.innerWidth > 1024
? createHTMLFromGrid(createGrid(images, 2, 3))
: createHTMLFromGrid(createGrid(images, 4, 3));
// append the HTML to your DOM
$(html).appendTo("body");
You can also change the grid based on the window resize event. Once you've got the HTML ready, you can play with the GSAP animation.
See CodePen

How to update deeply nested state in react?

I'm creating a sudoku board which renders JSX div objects in a grid like this.
newBoard.push(<div className={letnum} key={letcol} id={letcol}
onClick={() => this.handleTileClick(event)}>
{response.data[i][j]}
</div>
newBoard is then added to state, however when I click on a tile to change the innerHTML to a new number, I also want to update state at the same time. I get the error:
Cannot assign to read only property 'children' of object '#<Object>'
and I need to update:
this.state.board[index].props.children
to be a new number. Any advice on how to do this? I've been at it for hours haha. Here is the total code.
axios.get("http://127.0.0.1:5000/new-board")
.then(response => {
console.log("API Response: ", response)
for (let i = 0; i < 9; i++) {
let letter = String.fromCharCode(97 + i)
for (let j = 0; j < 9; j++) {
let column = j + 1;
let number = dictionary[j + 1];
let letnum = letter + " " + number + " tile";
let letcol = letter + column;
newBoard.push(<div
className={letnum}
key={letcol}
id={letcol}
onClick={() => this.handleTileClick(event)}>
{response.data[i][j]}
</div>)
}
}
this.setState({
board: newBoard,
winningBoard: newBoard,
gameWon: false
})
})
handleNumberChoiceClick(event) {
this.setState({
activeNumberSelector: Number(event.target.innerHTML)
})
}
handleTileClick(event) {
const selectedTile = event.target;
selectedTile.innerHTML = this.state.activeNumberSelector;
const location = this.state.board.indexOf((this.state.board.find(tile=>tile.props.id === selectedTile.id)));
this.setState({
activeNumberSelector: ''
})
console.log(this.state.board[location].props.children)
}
Simply: You shouldn't put React elements in state.
Only keep the data you need to render those elements in state (and props), and return those elements in your render function.

Delayed redux dispatch call speeds up UI update

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();

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])];

Hiding page numbers from pagination in Reactjs

I have a basic components with working pagination displaying props in a table.
This however shows all the page numbers which is not ideal I would like only a few to be seen.
e.g 1-2-3-4-5-20 so that it does not take up too much space.
This is the component itself.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
class AllAlerts extends Component {
state = {
currentPage: 1,
alertsPerPage: 8,
};
componentDidMount() {}
handleClick(number) {
this.setState({
currentPage: number,
});
}
render() {
// Logic for displaying alerts
const { currentPage, alertsPerPage } = this.state;
const indexOfLastAlerts = currentPage * alertsPerPage;
const indexOfFirstAlerts = indexOfLastAlerts - alertsPerPage;
const currentAlerts = this.props.alerts.slice(indexOfFirstAlerts, indexOfLastAlerts);
const renderAlerts = currentAlerts.map((alerts, index) => {
return (
<li key={index}>
<div>
All Title {alerts.title}
<li>All ID {alerts.id}</li>
<li>All user Id {alerts.userId}</li>
</div>
</li>
);
});
// Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(this.props.alerts.length / alertsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map((number) => {
return (
<button key={number} id={number} onClick={() => this.handleClick(number)}>
{number}
</button>
);
});
return (
<>
<>All Alerts</>
<br />
<form class="example" action="/action_page.php">
<input type="text" placeholder="Search.." name="search" />
<button type="submit">
<i class="fa fa-search"></i>
</button>
</form>
<>{renderAlerts}</>
<>{renderPageNumbers}</>
</>
);
}
}
export default AllAlerts;
From my understanding I would change this in this part of the code.
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(this.props.alerts.length / alertsPerPage); i++) {
pageNumbers.push(i);
//Only push 1-5 and last which has to change depending on page number
}
I have tried a few different approaches all not achieving what I would like.
Thank you for your help :)
Just check if the index i of the loop fits your condition
const pageNumbers = [];
const numPages = Math.ceil(this.props.alerts.length / alertsPerPage);
for (let i = 1; i <= numPages; i++) {
//Only push 1-5 and last which has to change depending on page number
if (i <= 5 || i == numPages)
pageNumbers.push(i);
}
But probably you should show the currentPage and the one before and after to, otherwise you won't be able to jump to the next or previous page
const pageNumbers = [];
const numPages = Math.ceil(this.props.alerts.length / alertsPerPage);
for (let i = 1; i <= numPages; i++) {
if (i <= 5 || //the first five pages
i == numPages || //the last page
Math.abs(currentPage - i) <= 1 //the current page and the one before and after
)
pageNumbers.push(i);
}
The best way would be to loop over page numbers, using the counter that react map method gives you can check how many buttons you have created and when then are more than five you stop creating buttons
{
pageNumbers.map((number,count)=>
count <5?
<button
key={number}
id={number}
onClick={() =>this.handleClick(number)}
>
{number}
</button>:
null
)
}
{pageNumbers.length >5? //next button here : null }
You then can go ahead and check the length of number of pages if creater than button you created you add next button
You shoulld update current page to state then check the current page if it is the first element in yhe page numbers else add previous button

Categories