by Phil Parsons
After the last article the development workflow is setup ready for us to start iterating on the code needed to build the application features. In this article we are going to connect two of the chat clients we have built so far using a web socket connection with Socket.io.
This post is part of the Developing for a modern web with React.js series. If you’d like to be notified of new posts in this series you can become a free member. If you don’t want to complete the previous articles before working through this tutorial you can download the code from Github.
Start by installing the Socket.io package with NPM.
npm install socket.io — save
The socket.io package allows us to create a socket server that will handle connections from clients and direct chat messages between them. A socket server allows for real time bidirectional messaging between a client and server through a dedicated connection so that clients do not need to poll the server for changes. You can read more about web sockets on the MDN developer network. Create a new folder under the server directory named socket-server and create the new file ./server/socket-server/index.js with the following code.
This socket-server module exports a single function that takes the Express application server as its only parameter. This simple implementation will serve us for the purpose of this article so that we can test the message passing between two browser tabs running the chat application. The socket server receives a message from one client and emits that same message to all other connected clients (we only want two to test out our chat for this article but it would work with more). Each socket connection is stored in an array called connections and when a message is received on a socket the list of connections is iterated over emitting the message to each one except for the socket that sent the message. When the socket disconnects it is removed from the connections array by splicing.
To initialise the socket server edit ./index.js to import the socket-server module and call the exported function passing it the Express application server.
Now when the Express application server is started the socket server is running and waiting for clients to connect. To create a connection add the following code to the client application in the new file ./client/chat.js.
In this module we include the client library that is bundled with Socket.io and create a connection to the server on the same host as our express application. To enable the communication between clients we need to be able to emit each added message to the server and listen for messages from other clients. First we’ll add the code to listen for a message and dispatch a new action to add the received message to the chat, let’s refer to these messages as responses. Edit ./client/chat.js and wrap the socket connection in an exported function that takes the store as it’s only argument.
The message action creators are imported and a the new action creator addResponse is used to create the action that is dispatched to the store when a message is recevied on the socket. Edit ./client/index.js to import the chat module and start the chat once the store is created.
Add the new addResponse action creator in ./client/actions/message-actions.js.
The new action needs to be handled in the reducer function so that the new message is added to the list of messages. Adding another case to the switch statement in the reducer function starts to raise a bit of a code smell. Look at how this might look.
There is code duplication and we can see that the addition of any more actions will face similar problems. We could move shared code to other helper functions but it’s at this stage that we need to make a better choice and refactor the code. Redux provides a utility called combineReducers that can be used to compose the separate parts of the state in the store using a combination of reducer functions. To achieve the level of separation wanted we need to move the logic that trims the message and checks that it is not empty to the MessageEntryBox component. First edit ./client/reducers/index.js to incorporate combineReducers.
There are now two separate functions for managing the individual sections of the state, currentMessage and messages, and it’s now much easier to understand how these parts of the state change with each action. Edit the MessageEntryBox component in ./client/components/message-entry-box/index.js to send the trimmed message on submit.
Lastly update the action creator to include the message.
Adding the Redux chat middleware
To send messages to the server we are going to create a middleware for Redux. Edit ./client/chat.js and make the following changes to export a middleware function called chatMiddleware.
Now every action will pass through this middleware function when dispatched and the two way communication with messages is established. Build and run the application using the npm scripts added in the last article. Once running open the chat application in two separate browser tabs and start a conversation with yourself. You should see the messages coming into the chat from the client in the other browser tab as you add them.
It’s working but the chat window is looking like it needs some styling and we can’t yet distinguish which message came from who. Before styling the chat window components let’s get the shape of the data for a message sorted.
We need to be able to identify each user so that we can assign the users identifier to each message. This way we can check the user id on each message to determine which messages are responses. Modify the socket server code to emit a new start event on successful connection that assigns an id to the client. Until we have a database installed we will use a simple counter for the user id. Edit ./server/socket-server/index.js and add the start event with the userId counter.
On the client we need to handle the start event by creating a new action to set the user id in the store. Edit ./client/chat.js and add the code to handle the start event.
Define the setUserId action creator in ./client/actions/message-actions.js
And add the reducer function for managing the userId attribute in ./client/reducers/index.js.
With these changes we now have the userId saved in the store state so we need to add it to the initial state we have in our server code. Edit ./server/index.js and add an empty string for the initial userId attribute.
The userId needs to be added to each message but we don’t want to muddle the concerns in our reducer functions. What we will do is replace the id we currently add to each message and compose the whole message from the MessageEntryBox component. Edit ./client/components/app/index.js and map the new userId property to the component props.
The userId is now passed into both the MessageList and MessageEntryBox components. Update ./client/components/message-entry-box/index.js to compose the message object with the text and user id.
Now both the ADD_RESPONSE and ADD_MESSAGE actions can be handled in the same way. We do need to keep the distinction though as the messages are sent to the socket server when the ADD_MESSAGE action is dispatched. Edit./client/reducers/index.js and refactor the messages reducer function.
Composing the message object in the component will also allow us to simplify the chat middleware as the whole message is available in the action. Refactor the middleware in ./client/chat.js.
Last thing that needs to be done is to edit the MessageList component and add a class name to messages that are responses. Edit ./client/components/message-list/index.js and add the is-response class name to every message where the userId does not match the userId in the component props. The HTML structure is also updated so that the messages can be styled more easily.
Styling the components
Okay, it’s about time we addressed the chat window styling. Feel free to style it how you would like or alternatively use the styles for the components shown below.
First edit ./client/components/app/style.less
then create the style sheet ./client/message-entry-box/style.less not forgetting to import it at the top of ./client/message-entry-box/index.js.
Last of all, create ./client/components/message-list/style.less again not forgetting to import it in the component index.js file.
Restart the servers if not already running and make conversation again between two browser tabs. You should see response messages shown on the other side of the chat in a different colour to the main messages.
There is quite a lot to take in from this article especially the way the code is refactored as more functionality is added. Refactoring is a common practice during development and a skill that is developed over time. Ways to separate concerns and reduce duplication where identified and addressed making the code easier to read and ultimately more maintainable.