Когда я работал над приложением Sudoku, я принял сознательное решение использовать Redux для управления состоянием. В моем блоге под названием Как я создал веб-приложение для судоку я сказал, что попробую использовать только Context API для управления состоянием приложения в будущем проекте. .

Этот проект был завершен вчера, когда я решил, что лучше потратить время на что-нибудь интересное, чем рыться в Reddit и Blind в поисках последних обновлений всей саги SVB. Я создал игру Snake на основе React.

Игра доступна бесплатно на http://sean-snake.netlify.com/. Проверьте это и дайте мне знать, сможете ли вы дойти до конца. Исходный код доступен по адресу https://github.com/seanluong/snake.

На высоком уровне это приложение на основе React, написанное на TypeScript с React MUI в качестве библиотек компонентов. Хотя это приложение (пока) не требует какой-либо внутренней работы (ознакомьтесь с моим сообщением в блоге о внутренней части приложения судоку здесь), оно сопряжено с изрядной долей проблем и интересным извлеченным уроком. В этом посте мы обсудим 3 моих любимых момента в работе над этим проектом.

Обработка событий клавиатуры

В то время как приложение судоку в основном управляется щелчком мыши, за исключением случаев, когда пользователям нужно вводить числа, природа игры Змейка требует более активного взаимодействия между пользователем и игрой. На рабочем столе, вероятно, имеет смысл разрешить людям перемещать змею и изменять ее направление, нажимая клавиши со стрелками.

Разница между обработкой событий щелчка мыши и событий нажатия клавиши заключается в том, что проще определить элемент, с которым должен быть связан обработчик события для щелчка мыши, чем для нажатия клавиши.

Для игры «Змейка» нам бы хотелось, чтобы обработчик событий нажатия клавиш был связан со всем HTML-документом, чтобы, пока пользователи фокусируются на документе, события обрабатывались. Это было сделано с помощью комбинации useEffect и useRef следующим образом

// Use useRef to have a reference to the `document` object
const documentRef = useRef<Document>(document);

useEffect(() => {
  // Check if the document is available for access
  if (documentRef.current) {
    const document = documentRef.current;
    // Add the event listener
    document.addEventListener('keydown', handleKeyDowned);

    // Clean up
    return () => {
      document.removeEventListener('keydown', handleKeyDowned)
    }
  }
}, [documentRef]);

Поддержка пользователей на мобильных устройствах

Хотя для пользователей настольных компьютеров, вероятно, лучше всего взаимодействовать с игрой с помощью событий нажатия клавиш, мобильным пользователям, вероятно, будет проще играть в игру, нажимая на экран. Это одна из проблем игры «Змейка», с которой я не сталкивался в приложении «Судоко».

Сначала я думал о том, чтобы позволить людям нажимать в любом месте на доске. Обработчик событий определяет, где находится цель, на которую нажали, по отношению к текущему положению змеи, и соответственно вычисляет новое направление. Одна потенциальная проблема UX с этим подходом заключается в том, что касание доски может помешать пользователям получить полное представление об игре из-за присутствия их собственных пальцев. Поскольку я не хотел, чтобы пользователи касались доски пальцами, я решил создать на экране кнопки для каждого из 4 направлений движения. Окончательный интерфейс выглядит так:

Я использовал react-device-detect, чтобы проверить, играет ли пользователь с мобильного или с рабочего стола.

Управляйте игровым состоянием

Context API позволяет сделать определенные фрагменты данных доступными для компонентов-потомков без необходимости сверления реквизита. В отличие от Redux, который является внешней библиотекой, Context API встроен в React, поэтому нет необходимости устанавливать другую зависимость. Еще одна приятная особенность Context API заключается в том, что мы можем воспользоваться крючком под названием useContext для доступа к данным, хранящимся в контексте.

Чтобы использовать Context API для управления состоянием, мы можем создать контекст на верхнем уровне приложения для хранения объекта состояния приложения. Чтобы обновить состояние приложения, нам нужно использовать useReducer, крючок React, который предназначен для управления нетривиальным локальным состоянием компонента. Если состояние не является примитивным значением (например, числом, строкой или логическим значением и т. д.), вам, вероятно, следует использовать useReducer вместо useState.

Создайте поставщика контекста для состояния

В игре Snake я определил компонент GameStateProvide

interface GameStateContextType {
  gameState: GameState;
  dispatch: (action: Action) => void;
}

const GameStateContext = createContext<GameStateContextType>({
  gameState: {} as GameState,
  dispatch: () => {},
})

const GameStateProvider = ({ children }: PropsWithChildren) => {
  const [gameState, dispatch] = useReducer(reducer, /* initial game state */);
  return (
    <GameStateContext.Provider value={{ gameState, dispatch }}>
      {children}
    </GameStateContext.Provider>
  );
}

затем использовал GameStateProvider на верхнем уровне приложения следующим образом

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <GameStateProvider>
      <App />
    </GameStateProvider>
  </React.StrictMode>,
)

Благодаря тому, что GameStateProvided обернуто вокруг App, все данные, хранящиеся в GameStateContext, теперь доступны для App и любого его потомка.

Доступ к данным в контексте состояния игры

Хотя useContext достаточно, чтобы любой компонент в нашем дереве компонентов мог получить доступ к состоянию приложения, более удобно определить собственный хук. Это было сделано с помощью кода ниже:

const useGameStateContext = () => useContext(GameStateContext);

Когда компонент хочет получить доступ к состоянию приложения, мы можем вызвать useGameStateContext следующим образом.

const { gameState, dispatch } = useGameStateContext();

gameState объект — это состояние приложения. dispatch — это функция, возвращаемая вызовом useReducer. Мы вызываем dispatch всякий раз, когда нам нужно отправить действие, которое будет обработано редьюсером приложения. Этот шаблон должен быть знаком людям, знакомым с Redux.