Reputation: 885
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!
Upvotes: 3
Views: 6487
Reputation: 3077
the issue is inside of Board.jsx where you are getting the State Data from useContext, you have to get values as an array, example:
const [
state: {snake, food, direction, gameOver},
dispatch,
rows,
cols
] = useContext(snakeContext)
Upvotes: 0
Reputation: 885
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!
Upvotes: 2
Reputation: 707
The handling of the SNAKE
action in the reducer doesn't seem to be right. You're returning an array but you're probably expecting a state like your initial state, right?
const initialState = {
snake: [{ x: 0, y: 0 }],
prev: { x: null, y: null },
food: { x: randomPosition(rows), y: randomPosition(cols) },
direction: null,
gameOver: false
};
The return value of the reducer for the SNAKE
action is something like this though, since snake[0]
is { x:..., y: ...}
:
[{ x: payload.x, y: payload.y }]
Upvotes: 0
Reputation: 570
Could you update this please?
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:
return state; //error here: throw new Error();
}
};
Upvotes: -1