To get stuck in with react hooks I decided to try and make snake and use useReducer/useContext state management.
Right now I'm blocked where I want a directional keypress to change the state of the active tile. In useReducer this seems to receive the correct payload, and create the correct payload object, but when it updates the state it is undefined?
Pressing the down key gives an error of "TypeError: Cannot read property 'some' of undefined" meaning that snake is undefined.
Board.jsx
import React, { useContext, useEffect } from "react";
import Tile from "./Tile.jsx";
import { snakeContext } from '../contexts/snakeContext'
const Board = () => {
const {
state: {
snake, food, direction, gameOver
},
dispatch,
rows,
cols
} = useContext(snakeContext)
useEffect(() => {
const onKeyPress = (e) => {
switch (e.keyCode) {
case 38: //Up
return direction === "down" || dispatch({ type: 'DIRECTION', payload: "up" });
case 40: // Down
return direction === "up" || dispatch({ type: 'DIRECTION', payload: "down" });
case 37: //Left
return direction === "right" || dispatch({ type: 'DIRECTION', payload: "left" });
case 39: // Right
return direction === "left" ||
dispatch({ type: 'DIRECTION', payload: "right" });
default:
break;
}
};
window.addEventListener("keydown", onKeyPress);
return () => window.removeEventListener("keydown", onKeyPress);
}, [direction]);
useEffect(() => {
const interval = setInterval(() => {
switch (direction) {
case "up":
dispatch({ type: 'SNAKE', payload: { ...snake[0], y: snake[0].y - 1 } })
break
case "down":
dispatch({ type: 'SNAKE', payload: { ...snake[0], y: snake[0].y + 1 } })
break;
case "left":
dispatch({ type: 'SNAKE', payload: { ...snake[0], x: snake[0].x - 1 } })
break;
case "right":
dispatch({ type: 'SNAKE', payload: { ...snake[0], x: snake[0].x + 1 } })
break;
default:
break;
}
}, 500);
return () => clearInterval(interval);
});
const style = {
maxHeight: `${2 * rows}rem`,
maxWidth: `${2 * cols}rem`,
margin: "0 auto",
paddingTop: "4rem"
};
const isActiveMatchingState = (i, j) => {
return snake.some(snakeTile =>
snakeTile.y === i && snakeTile.x === j
)
}
const renderBoard = () => {
let grid = Array.from(Array(rows), () => new Array(cols));
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
grid[i][j] = (
<Tile
isActive={isActiveMatchingState(i, j)}
isFood={food.y === i && food.x === j}
key={`${[i, j]}`}
/>
);
}
}
return grid;
};
return (
gameOver ?
<div>GAME OVER</div> :
<div style={style}>{renderBoard()}</div>
)
};
export default Board;
snakeReducer.jsx
export const snakeReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'SNAKE':
return [{ ...state.snake[0], x: payload.x, y: payload.y }]
case 'FOOD':
return { ...state, x: payload.x, y: payload.y };
case 'DIRECTION':
return { ...state, direction: payload };
case 'GAME_OVER':
return { ...state, gameOver: payload };
default:
throw new Error();
}
};
My useContext setup uses useMemo as suggested - https://hswolff.com/blog/how-to-usecontext-with-usereducer/
snakeContext.js
import React, { createContext, useReducer, useMemo } from 'react';
import { snakeReducer } from '../reducers/snakeReducer';
export const snakeContext = createContext();
const rows = 20;
const cols = 15;
const randomPosition = (biggestNumber) => Math.floor(Math.random() * biggestNumber)
const initialState = {
snake: [{ x: 0, y: 0 }],
food: { x: randomPosition(rows), y: randomPosition(cols) },
direction: null,
gameOver: false
};
const SnakeContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(snakeReducer, initialState);
const contextValue = useMemo(() => ({ state, rows, cols, dispatch }), [state, dispatch]);
return <snakeContext.Provider value={contextValue}>{children}</snakeContext.Provider>;
};
export default SnakeContextProvider;
App.js
import React from 'react';
import Home from './pages/Home';
import SnakeContextProvider from './contexts/snakeContext';
import './App.css';
const App = () => {
return (
<SnakeContextProvider>
<Home />
</SnakeContextProvider>
)
};
export default App;
Home.jsx is a page component which contains Board.jsx
The strange thing is that the update on the direction keypress updates fine, so the useReducer seems to be setup correctly.
Full current repo is here - https://github.com/puyanwei/snake
Thanks!
It was my reducer in the end, the correct return for 'SNAKE' should be;
case 'SNAKE':
return {
...state,
snake: [{ x: payload.x, y: payload.y }, ...state.snake]
};
Thanks all who helped!