简体   繁体   中英

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!

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!

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 }]

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)

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM