Search code examples
javascriptreactjsreact-hooksreact-state-management

useReducer returning undefined rather then array of objects


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!


Solution

  • 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!