Selective state not updating in React component
I'm trying to create a basic snake game in React. I seem to be misunderstanding state. Right now the player can move, and the cherry gets placed for pickup, however when checking for collision inside of the move() function, I can't seem to update gameOver state. Is there something obvious I am missing? Appreciate you taking the time to read and respond.
import { useEffect, useState } from "react";
const Snake = () => {
const timer = (ms) => new Promise((res) => setTimeout(res, ms));
let [gameStarted, setGameStarted] = useState(false);
let [gameOver, setGameOver] = useState(false);
let [playerLocation, setPlayerLocation] = useState({ x: 20, y: 20 });
let [cherryLocation, setCherryLocation] = useState({});
let direction = "W";
const tableWidth = 40;
const tableHeight = 40;
const cellSize = "10px";
const handleKeyPress = (e) => {
switch (e.code) {
case "ArrowLeft":
direction = "W";
break;
case "ArrowRight":
direction = "E";
break;
case "ArrowUp":
direction = "N";
break;
case "ArrowDown":
direction = "S";
break;
}
};
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min);
}
const placeCherry = () => {
return {
x: getRandomInt(0, tableWidth),
y: getRandomInt(0, tableHeight)
};
};
const checkCollision = () => {
if (playerLocation.x <= 0 || playerLocation.x >= tableWidth || playerLocation.y <= 0 || playerLocation.y >= tableHeight) {
return true
}
return false
};
const startGame = () => {
if (!gameStarted) {
setGameStarted(!gameStarted);
setGameOver(!gameOver);
update();
}
};
const move = () => {
let newX = playerLocation.x;
let newY = playerLocation.y;
switch (direction) {
case "N":
newY = playerLocation.y -= 1;
break;
case "S":
newY = playerLocation.y += 1;
break;
case "E":
newX = playerLocation.x += 1;
break;
case "W":
newX = playerLocation.x -= 1;
break;
}
if(checkCollision()) {
setGameOver(!gameOver);
}
setPlayerLocation((prevState) => ({
...prevState,
x: newX,
y: newY
}));
};
const update = async () => {
setCherryLocation(placeCherry());
while (!gameOver) {
console.log(gameOver);
await timer(100);
move();
}
alert("Game Over!");
};
const renderBoard = () => {
return (
<table style={{ border: "2px solid rgba(0, 0, 0, 0.05)" }}>
<tbody>
{[...Array(tableHeight)].map((y, i) => {
return (
<tr key={i}>
{[...Array(tableWidth)].map((x, j) => {
if (playerLocation.y === i && playerLocation.x === j) {
return (
<td
style={{
width: cellSize,
height: cellSize,
backgroundColor: "green"
}}
key={j}
>
{" "}
</td>
);
}
if (cherryLocation.y === i && cherryLocation.x === j) {
return (
<td
style={{
width: cellSize,
height: cellSize,
backgroundColor: "pink"
}}
key={j}
>
{" "}
</td>
);
}
return (
<td
style={{
width: cellSize,
height: cellSize,
backgroundColor: "black"
}}
key={j}
>
{" "}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
};
useEffect(() => {
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, []);
return (
<div>
<p>Snake</p>
<div style={{ display: "flex", justifyContent: "center" }}>
{renderBoard()}
</div>
<div>
<button
disabled={gameOver}
style={{ width: "250px", height: "50px", marginTop: "30px" }}
onClick={startGame}
>
Start
</button>
</div>
</div>
);
};
export default Snake;
Solution 1:
The gameOver
state was not being updated inside the async function,which leads to an infinite while loop. So I tried to replace the timer with setInterval updater function.
Replace the existing functions with following to proceed.
const timerIdRef = useRef();
useEffect(() => {
if (gameOver) {
alert("Game Over!");
}
if (!gameOver && !checkCollision()) {
setCherryLocation(placeCherry());
}
return () => clearInterval(timerIdRef.current);
}, [gameOver]);
const handleKeyPress = (e) => {
switch (e.code) {
case "ArrowLeft":
direction = "W";
update();
break;
case "ArrowRight":
direction = "E";
update();
break;
case "ArrowUp":
direction = "N";
update();
break;
case "ArrowDown":
direction = "S";
update();
break;
}
};
const startGame = () => {
if (!gameStarted) {
setGameStarted(true);
setGameOver(false);
update();
}
};
const move = () => {
let newX = playerLocation.x;
let newY = playerLocation.y;
switch (direction) {
case "N":
newY = playerLocation.y -= 1;
break;
case "S":
newY = playerLocation.y += 1;
break;
case "E":
newX = playerLocation.x += 1;
break;
case "W":
newX = playerLocation.x -= 1;
break;
}
if (checkCollision()) {
setGameOver(true);
}
setPlayerLocation({
x: newX,
y: newY
});
};
const update = () => {
clearInterval(timerIdRef.current);
if (!gameOver) {
timerIdRef.current = setInterval(() => move(), 100);
}
};