In Part 1 we setup the repository and implemented basic Front End using React. At the moment the app isn't so fun because we can't even play the game. In this part we will build the server and then wire the logic all together. By the end, we'll have a playable TicTacToe game!
If you wish to see the full code, you'll find it in this GitHub repository.
Table of Content
- Technologies Involved
- Project Scope
- Step 1: Create a new Project
- Step 2: Create the Front End
Part 2: Server & State (we're here!)
- A Real Time Web Server
- The HTTP Server
- The Socket.IO Server
- State Management with Zustand
- Creating a Zustand store
- Using the Store
A Real Time Web Server
Now to the heart of this project: the Server. We need two parts: one HTTP server and one Socket.IO Server
The HTTP Server
We will create an HTTP server and request our Socket.IO instance to listen to it. We could create a separate app only for the server but Next.js offers a capability to create our own server on top of theirs, so let's use that.
If we check Next.js's documentation for custom servers, this is what we would need:
Before we go further, let's talk about what we just did:
First, we created a Next.js app using
const app = next(/*opts*/);. This app handles all the logic related to Next.js. So we are free to build our own server, using the stack we wish and make sure we forward the remaining requests to the
app, thanks to
After having the app, we called its
prepare method so we could create our server. I used
createServer from the
http module. But I could have used
fastify or something else. What's important is that we need an HTTP server.
In our use case, we don't need our server to handle any route but a Socket.IO server. So we forward any requests to the
handle method and we are done with it.
We should be able to run
yarn dev to start the app, with the logs showing
> Server listening at http://localhost:3000 as development
Finally, we can bind our HTTP server to our Socket.IO server!
Note: The reason we did this instead of using the Next.js API routes is because Real time communication doesn't work with them. At some point the server will drop the connection to clean up resources.
The Socket.IO Server
Now that we have our HTTP server, we can create our Socket.IO Server.
Let's take a break and see what Socket.IO means, how it is different from an HTTP server and why we need both.
About what Socket.IO is:
Socket.IO is a library that enables real-time, bidirectional and event-based communication between the browser and the server
In comparison, with regular HTTP servers, a user can make a Request and that Server can only Respond to it. Once the Response is sent, the connection is closed. There's also no way for the server to send information to a user if that user didn't make a Request first.
With Real Time Communication (RTC), we can keep a long lived connection and send messages from both sides at any time.
It is most useful when multiple users are connected to the same server, like a chat application. When a user writes a new message, all users should be notified. For the sake of our game, using a WebSocket Server would also work.
Socket.IO in the end is a library that implements the WebRTC technologies we need. The main technology involved in WebRTC is WebSocket. WebSocket is a communication protocol (like HTTP) that allows bi directional communication.
We could have used a number of different libraries or even used WebSocket by hand, but that's too much of a hassle and I do like socket.IO. The library is easy to use.
Then, do we need both? A Socket.IO server can run on its own but we still need an HTTP server to make our React App work. It would force us to create a repository and deploy it just for that purpose. Instead, because of the similarities between WebSocket and HTTP, we can make them share the same port quite easily and that's just what we do!
Now I hope you have a tiny better understanding of what it is so let's see how to use it.
For this part, I'll implement the server in a slightly different way than I did in the repository. Because in the repository, I have a little bit more code for type-checking and utility functions. In the article I prefer to focus on the core of the subject.
Let's pause here already. We just created a new class that when constructed, will start a Socket.IO server, listen to the
httpServer's port and then listen to the following events:
Those are the two events that the user can send to the server. Everything else will be ignored. The
disconnect event is a bit special as it's called when the connection is closed.
If we were to compare this to an HTTP server, it would look something like:
The main difference is that the
client parameter is not dropped until the connection is explicitly closed. This allows us to do the next step: replying back to the client!
With this last piece of code, we have what we need to be able to play a TicTacToe game against a bot in real time! In order to test it, we will connect to it in the React app using socket.io-client library. But first we need to decide on how we will manage our React state.
State Management with Zustand
Now that we have a nice server that can handle all the logic for our game, we can connect our React App to it!
React is a library to build UI. Even if it does provide a way to internally handle state changes, the business logic and data fetching is left up to the developer to choose their tools and implementations. For this project, we have to answer the following question: how do we automatically react to both user and server events in our React Application?
There are many answers to this question. The most simple one would be to handle all this logic inside the main component's state itself. The downside of this solution is that we would have poor separation of concerns as our component would be in charge of dealing with the global state changes, handling user requests and managing the server updates. Another way would be to use Redux and have our events wrapped inside thunks, actions creators & co.
I wanted to settle for something that would let me hide the implementation details behind a nice API without having to rely on a complex Redux configuration. A while ago I stumbled upon Zustand. It's small, easy to understand yet the most powerful one I found so far.
Creating a Zustand store
Let's see how Zustand works:
Our state management for this app holds in a few lines. But it already shows that we can configure it as we like and connect it to other data sources quite easily. Now let's see how to use it!
Note: In the repository, I added a few helper tools to help the Type checker. You'll see a few more line of codes where I import the interfaces for the different entities and have a wrapper around the socket.io-client object. I omitted those in this article to keep things concise.
Using the Store
We already have a hint on how the Zustand store works: it's a React hook! We can spot it because we named our component
useStore rather than
Let's go back to our Game component (src/components/Game.tsx) and remove all the mock data and use our store!
And that's it! We use our store similarly as if it was a React.useMemo, we get a callback function and return the values we want. It's best to return only the data we need because Zustand will smartly update the component if they change. If we were to return the entire store
const everything = useStore(store => store) then the component would re-render any time any field of the store would change.
With that, if your server is properly working, you should be able to play the game! Enjoy.
This is a very small application but it does feature everything needed to setup a real time app. Next you could attempt to improve the AI and make it smarter (or even unbeatable with a min max algorithm). Or make it playable with two remote players. Or completely create a new game or app, the skeleton should be enough to build something else.
Again, we focused on the minimal requirements here. Looking at this an HTTP server would have been enough. A more concrete example for WebSocket use case is when you want to build a chat system: many users connect to the same server and when a new message is sent, the server forwards them to all the users.
There's plenty of different technologies to achieve the same goal, here the objective was to focus on Real Time Communication implementation with the minimum amount of code (even though we spent a lot of time on things like setting up yarn 2 and the next app).
Hope you enjoyed this series of articles!