How I built a React-based Snake game with Context API

Sean
SLTC — Sean Learns To Code
5 min readMar 13, 2023

When I worked on the Sudoku app, I made a conscious decision to use Redux for state management. In my blog post titled How I built a web-based Sudoku app, I said that I would give it a try to use just the Context API to manage the app state in a future project.

That project was finished yesterday as I decided that it would be better to spend my time working on something fun rather than lurking over Reddit and Blind for the latest updates on the whole SVB saga. I built a React-based Snake game.

The desktop UI of my React-based Snake game

The game is available for free at http://sean-snake.netlify.com/. Check it out and let me know if you’re able to reach the end of it. The source code is available at https://github.com/seanluong/snake.

At a high level, this is a React-based application that was written in TypeScript with React MUI as the component libraries. While this app (so far) doesn’t involve any backend work (check out my blog post about the backend part of the Sudoku app here), it does come with a fair share of challenges and interesting lesson learned. This post will discuss my 3 favorite things about working on this project.

Handle keyboard events

While the Sudoku app is mostly mouse click driven except for when users have to enter numbers, the nature of the Snake game requires more active interaction between a user and the game. On desktop it probably makes the most sense to allow people to move the snake and change its direction by pressing arrow keys.

The difference between handling mouse click events and key press events is that it’s more straightforward to identify the element to which the event handler should be linked for a mouse click than for a key press.

For the Snake game, we would want the event handler for the key press events to be linked to the whole HTML document so that as long as users focus on the document the events will be handled. This was done by using a combination of useEffect and useRef as follows

// 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]);

Support users on mobile devices

While it’s probably best for desktop users to interact with the game via key press events, mobile users probable find it easier to play the game by tapping on the screen. This is one of the challenges of the Snake game poses that I didn’t face with Sudoko app.

At first, I thought of allowing people to tap any where on the board. The event handler would detect where the tapped target was with respect to the snake’s current position and calculate the new direction accordingly. One potential UX issue with this approach is that tapping on the board could prevent users from having a complete view of the game due to the presence of their own fingers. Because I didn’t want users to put their fingers on the board, I decided to create buttons on the screen for each of the 4 movement directions. The final UI looks like this:

The mobile UI of the Snake game

I used react-device-detect to check if a user was playing from mobile or from desktop.

Manage the game state

The Context API enables making certain pieces of data available to descendant components without having to do prop drilling. Unlike Redux, which is an external library, the Context API is built-in to React so there’s no need to install another dependency. Another nice thing about the Context API is that we can take advantage of a hook called useContext to access the data stored in the context.

To use the Context API for state management we can create a context at the top-level of the app to store the app state object. To update the app state, we need to use useReducer, a React hook that is meant to be used to manage non-trivial component’s local state. If the state is not a primitive value (e.g. a number, a string, or a boolean value, etc.) then you should probably be using useReducer instead of useState.

Create the context provider for the state

In the Snake game, I defined a GameStateProvide component

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

then used GameStateProvider at the top-level of the app as follows

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

By having GameStateProvided wrapped around App, all the data stored in the GameStateContext is now made available to App and any of its descendant.

Access the data in the game state’s context

While useContext is good enough for any component in our component tree to access the app state, it’s more convenient to define a custom hook. This was done by the code below:

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

When a component wants to access the app state, we can call useGameStateContext as follows

const { gameState, dispatch } = useGameStateContext();

gameState object is the app state. dispatch is a function that is returned by calling useReducer . We call dispatch whenever we need to dispatch an action that will be processed by the app’s reducer. This pattern should be no stranger to people who are familiar with Redux.

--

--