Inspired by taming snakes with reactive streams using solely HTML5 and JavaScript together with RxJS, today I will use NextJS along with RxJS to create a game called Next Snake. Without further ado, let's experience the real effect (not yet adapted to the mobile page).
Preliminary design
Firstly, I plan to bootstrap the project with create-next-app
, which is the easiest way to get started with Next.js. Then, to use RxJS in the React world, I will use a library called observable-hooks to connect the observable world and the React world. Therefore, the ideal approach is for the heavy logic to take place behind the observable streaming, and using that streaming data to drive React rendering state.
Using RxJS in React
observable-hooks provides a variety of observable React hooks to connect RxJS and React. Its core concept is about two world: the Observable World and the Normal World.

These two worlds are just conceptual partition. The Observable World is where the observable pipelines are placed. It could be inside or outside of the React components. The Normal World is anyplace that does not belong to the Observable World. In my opinion, it should be the React state. Let's take an example:
javascriptCopy code
- useObservable: Accept a function that returns an Observable. Optionally accepts an array of dependencies which will be turned into Observable and be passed to the epic function. Using
useObservable
to create or transform Observables and avoid repeated operations in React functional components.- useObservableState: Get values from Observables, the values can be used as states in React
- useSubscription: the achievement of RxJS subscribe in React.
- useObservableCallback: Returns a callback function and an events Observable. Whenever the callback is called, the Observable will emit the first argument of the callback. Can be treated as
fromEvent
in RxJS.- useObservableRef: Returns a mutable ref object and a BehaviorSubject. Whenever
ref.current
is changed, the BehaviorSubject will emit the new value.Code structure
Our entire logic takes place in the Snake
component. This component is designed like this:
typescriptCopy code
Apart from the stop
and gameOver
states, the core game data is the scene
state object, which contains the score
, apples
, and snake
states that can be rendered at the template. What's more, useScene
is not a normal React hook; inside it is a world of observable streams.
RxJS Streaming Implementation
Based on the requirements of the game, we learnt that we need the following features for our game.
Use arrow keys to steer the snake
Control the speed of movement of the snake
Record the player's score
Record the snake (including eating apples and moving)
Record apples (including generating new apples)
With the above functionality description let's design the RxJS flow we need.
Identifying the streams
direction$ stream
This stream is used to navigate the snake's movement. We need to listen to the keyboard arrow keys and map the key codes to our custom direction state.
typescriptCopy code
scan()
operator works a lot like Array.reduce()
, but instead of just giving you the final value, it outputs each step along the way. With scan()
, you can keep adding up values and reduce a stream of events to a single value continuously. This helps us track the previous direction without needing any external state.score$ stream
This stream is designed to be used as a broadcast, which can be piped to increase our snake's length. It starts with an initial score of 0.
typescriptCopy code
snake$ stream
This stream is more complicated since the snake changes with:
Time changes. We use something like an interval timer to drive the snake's movement.
typescriptCopy codeConsider that we need to control the snake's speed:
typescriptCopy codeDirection changes.
Snake length. It can be increased every time the score$ stream emits a new score.
typescriptCopy code
Therefore, we can combine the snake$
like this:
typescriptCopy code
snake$
will be used as an input for apple$
and also serves as a source stream for the game scene. This means we would end up recreating that source stream with the second subscriptions to the same Observable. At this point, we can use share()
operator.typescriptCopy code
apple$ stream
This stream mainly revolves around the snake. Each time the snake moves, we check if its head hits an apple. If it does, we remove the apple and place a new one randomly on the field. So, there's no need to create a separate stream for the apples.
typescriptCopy code
The final streams
Firstly, we will make some changes to our timeTicker$
. Since we use interval
to drive the movement of the snake, we can use requestAnimationFrame
to allow the browser to change the position more smoothly. RxJS offers an animationFrame
scheduler that can be used to create smooth browser animations. It ensures that scheduled tasks will happen just before the next browser content repaint, thus performing animations as efficiently as possible.
typescriptCopy code
Then, we prefer to stop the game whenever we do. So the final timeTicker$
should be like:
typescriptCopy code
Finally , we combine snake
, apple$
and score$
to generate and subscribe scene$
typescriptCopy codetypescriptCopy code
And each time when new apple appers, we increase our score
typescriptCopy code
You can check the whole RxJS code within React hooks: