useReducer возвращает undefined, а не массив объектов

Чтобы застрять с перехватчиками реакции, я решил попробовать сделать змейку и использовать управление состоянием useReducer / useContext.

Прямо сейчас я заблокирован там, где я хочу, чтобы нажатие клавиши со стрелкой изменило состояние активной плитки. В useReducer кажется, что он получает правильную полезную нагрузку и создает правильный объект полезной нагрузки, но когда он обновляет состояние, он не определен?

Нажатие клавиши вниз дает ошибку «TypeError: Cannot read property 'some' of undefined», что означает, что snake не определен.

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

В моей настройке useContext используется useMemo, как было предложено - 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 - это компонент страницы, который содержит Board.jsx

Странно то, что обновление направления нажатия клавиш обновляется нормально, поэтому кажется, что useReducer настроен правильно.

Полное текущее репо здесь - https://github.com/puyanwei/snake

Спасибо!


person pyan    schedule 11.11.2019    source источник
comment
Можете ли вы предоставить codeandbox?   -  person Fraction    schedule 12.11.2019


Ответы (3)


В конце концов, это был мой редюсер, правильное возвращение для "ЗМЕЯ" должно быть;

 case 'SNAKE':
            return {
                ...state,
                snake: [{ x: payload.x, y: payload.y }, ...state.snake]
            };

Спасибо всем, кто помогал!

person pyan    schedule 12.11.2019

Обработка действия SNAKE в редукторе кажется неправильной. Вы возвращаете массив, но, вероятно, ожидаете состояния, подобного исходному, верно?

const initialState = {
    snake: [{ x: 0, y: 0 }],
    prev: { x: null, y: null },
    food: { x: randomPosition(rows), y: randomPosition(cols) },
    direction: null,
    gameOver: false
};

Однако возвращаемое значение редуктора для действия SNAKE выглядит примерно так, поскольку snake[0] равно { x:..., y: ...}:

[{ x: payload.x, y: payload.y }]
person jkettmann    schedule 12.11.2019

Не могли бы вы обновить это, пожалуйста?

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();
    }
};
person Jikun L    schedule 12.11.2019
comment
Обновил это, без разницы - person pyan; 12.11.2019