In the age of the internet, we increasingly choose to meet indirectly, usually at home in front of a smartphone or laptop screen. Staying in touch with the whole world has become our everyday life. Not surprisingly, in the 21st century this is easier than ever before. You find a chat room, create an account, write your first message and... How does it all actually work? Can a beginner developer implement this? I'll show you how to code the basics of real-time chat in an easy way using the socket.io and React.

WebSocket - communication without refreshing

When I first came across Socket.io I immediately saw thousands of usages. WebSocket provides bidirectional communication between the client and the server. As a result, after the initial request, the client no longer needs to send a request for data. During that first client-server interaction (WebSocket handshake), a WebSocket connection is established. This paves the way for fast data exchange. Each client sending a message emits some event e.g. send_message. Next the server processes this request, then sends this message to each connected client (including the one who sent the message). Of course without refreshing, the data just appears on the client side!

socket.io bidirectional communication cartoon

But what do we actually gain from that? Imagine an application in which the data appearing on the server is immediately sent to each of the clients. No refreshing, no need to check whether new data has appeared on the server. Action/Reaction. If you haven't got a million application ideas in your head yet, below I will list some of mine:

  • Multiplayer browser game - play against your friends. Every time a player clicks on the keyboard, an event is triggered that is received by the other players. You turn right/left, you shoot. All in real time!
  • Retro board - you want to create the perfect room for a meeting with your team? Develop an application based on WebSockets. Add three columns (Glad/Sad/Actions), let the users add notes (sounds like a chat room with extra steps, right?). After the meeting you can generate logs from the server and have a report ready. Close the sessions and share the results with the team.
  • Chat - this is probably the most popular use of WebSockets (or at least the most used as a tutorial). Real-time chat is almost a textbook example. We add a new user and emit the add user event, send a message, exit the chat. All this just like that, without refreshing the application!

ChatMe.now

ChatMe.now is a messaging app created specifically for this article. In first realease, there are just basic functionalities like:

  • Choosing a nickname and joining a common chat room
  • Displaying active users
  • Sending messages to other users

I coded ChatMe.now app to show you how easy it can be to create a chat room using the socket.io library (keep in mind that a "real" messaging app would have a ton of security features, many layers etc. but this time I want to focus strictly on the basics). To create real-time chat you'll need a few things. First, a server. In my case it's:

Front-end stack:

Simplicity matters

Instead of setting up the application step by step, I used Create React App (of course I encourage you to set up the application yourself). A few clicks are enough to download the necessary packages. We are led by the hand through the entire configuration, which makes it really difficult to make a mistake.

$ mkdir front && cd front && npx create-react-app chat-me-now

Since we have already created a basic front-end config we can just as easily "generate" a server. The stack I used is Node.js/Express/Nodemon. Here we additionally need socket.io library. All of this we can get with one command:

$ touch index.js && npm install express socket.io && npm install --save-dev nodemon

The last thing I've managed to get used to in projects is concurrently. An npm package that allows us to call multiple commands simultaneously in one terminal:

npm i concurrently

Once you've added everything you need open the package.json file (in my case a file in the root directory) and add some scripts to your configuration:

"scripts": {
    "dev": "concurrently \"npm run server\" \"npm run client\"",
    "client": "cd frontend && yarn start",
    "server": "nodemon run"
  }

Blah... blah... blah... Code

Before you copy all the examples thinking you will create a chat - in 10 minutes! Stop for a moment. The following examples contain code for selected modules. Just copy and paste is not enough. The application is built with more than these few modules (but I didn't want to bore everyone with scrolling for eternity). I will add a link to my GitHub project at the end of the article.

If you have made it through the introduction then thank you for your time. If you have been scrolling in search for a ready-made server/application, this is not the time. The back-end code is a few dozen lines. To run the local server we need to import the express and socket.io (that's clear). Then we need to force listening on a specific port (in my case 4200) and create a basic connection via socket (line 19). In the body of the connection we will add some events handled by our chat. In this example, I will not use a database to store messages or a list of users to keep the implementation as simple as possible.

const express = require('express');
const socket = require('socket.io');
const addNewUser = require('./src/server/addNewUser');
const sendingMessage = require('./src/server/sendingMessage');
const userDisconnected = require('./src/server/userDisconnected');

const PORT = process.env.PORT || 4200;
const HOSTNAME = ‘http://localhost’;
const chatUsers = new Set();
const app = express();

const server = app.listen(PORT, () => {
	console.log(`Visit ${HOSTNAME}:${PORT}`);
});
const io = socket(server);

app.use(express.static('frontend/build'));

io.on('connection', (socket) => {
	addNewUser(socket, io, chatUsers);
	sendingMessage(socket, io);
	userDisconnected(socket, io, chatUsers);
});

The libraries we're using really simplify event handling. Our server works on the principle of action/reaction. Therefore, when emitting an event, we need to intercept it in order to execute the assigned series of commands. Suppose we want to add a new user (we emit add_new_user) - then listen for add_new_user. Check the user id and add them to the Set. Then we will send such a prepared Set to every client (including the one that just emitted ”add action”). When we receive the collection on the client side we can enjoy the list of active users.

Remember to assign the user's nickname to socket.userId (or wherever you want) so you can identify the user later.

module.exports = (socket, io, chatUsers) => {
  socket.on('add_new_user', data => {
    socket.userId = data;
    chatUsers.add(data);
    io.emit('add_new_user', [...chatUsers]);
  });
};

How does it look on the front-end? As we use socket.io-client, we have ensured the communication with the server. Let’s create an AddNewUser component displaying an input and a button for adding a new user. This button, when clicked, emits the add_new_user action with a nickname. Everything else is no different from coding an SPA using the API or the state itself. It is good practice to control properties (if you are not already using TypeScript) here I use PropTypes - I recommend consciously managing props, validation is extremely helpful if you want to avoid errors of unknown origin in the future.

import React, { useState } from 'react';
import socketIOClient from 'socket.io-client';
import PropTypes from 'prop-types';
import { AddUserButton, AddUserContainer, AddUserInput, AddUserLabel } from '../style/components/addNewUser';

const AddNewUser = ({ setCurrentUser }) => {
  const [user, setUser] = useState('');
  const inputID = 'add-user-id';
  const isUser = user.length;

  const addNewUser = () => {
    const socket = socketIOClient('http://localhost:4200');
    socket.emit('add_new_user', user);
    setCurrentUser(user);
  };

  const handleKeyDown = ({ key }) => {
    if (key === 'Enter' && isUser) {
      addNewUser();
    }
  };

  return (
    <AddUserContainer>
      <AddUserLabel htmlFor={inputID}>Enter your nickname</AddUserLabel>
      <AddUserInput
        placeholder="Nickname..."
        value={user}
        onChange={e => setUser(e.target.value)}
        onKeyDown={handleKeyDown}
      />
      <AddUserButton onClick={addNewUser} disabled={!isUser}>
        join
      </AddUserButton>
    </AddUserContainer>
  );
};

AddNewUser.propTypes = {
  setCurrentUser: PropTypes.func,
};

You already have functionality for adding users. This is the first and important step of building a chat - because we need to know who wrote to us. Let's get on with sending messages (after all, that's the essence of a chat, right?). To handle the sending of messages by the server we need to add another module that takes an object containing data about the user and the content of the message. The function takes the data from the sending user and sends it to all connected clients. In src/server/sendingMessage.js add and export the following module.

module.exports = (socket, io) => {
  socket.on('sending_a_message', data => {
    io.emit('sending_a_message', data);
  });
};

By now you're probably wondering "Is that it? That's all the messaging functionality?". If we're talking about basic chat then yes, that's it. This is where I leave room for you to do your own thing. You can add security, message encryption, and even plug in artificial intelligence that checks keywords and catches what our users are talking about. I'm not urging you, I'm just showing you the possibilities. And now, since we have the basis of the back-end, let's deal with the client.

import React, { useState } from 'react';
import socketIOClient from 'socket.io-client';
import PropTypes from 'prop-types';
import { MessageInputButton, MessageInputContainer, MessageInputForm } from '../style/components/messageInput';

const MessageInput = ({ currentUser }) => {
  const [message, addMessage] = useState('');
  const socket = socketIOClient('http://localhost:4200');

  const sendMessage = () => {
    if (!message) {
      return;
    }
    socket.emit('sending_a_message', { message, user: currentUser });
    addMessage('');
  };
  return (
    <MessageInputContainer>
      <MessageInputForm value={message} onChange={e => addMessage(e.target.value)} />
      <MessageInputButton>&gt;</MessageInputButton>
    </MessageInputContainer>
  );
};

MessageInput.propTypes = { currentUser: PropTypes.string };

export default MessageInput;

Now let's create a basic component that takes in the properties of the current user. I will skip any validation for now and only check if the message being sent is not empty. I will store the message content in a local variable using useState. Each click on the active input will call addMessage which will fill our message variable. Once we have added the message (e.g. saying hello to the rest of the users) we click the MessageInputButton to accept sending. The sendMessage() function emits the sending_a_message event with an object storing the message and its author. Thus prepared data goes to the server, and then to each user (also to the author).

All that remains is to display the messages of all users. For this we will use the above logic of spreading messages across clients. All we need to do is add a component to display the data.

import React from 'react';
import PropTypes from 'prop-types';
import MessageInput from './MessageInput';
import {
  MessageBoxContainer,
  MessageBoxWrapper,
  MessageBoxForm,
  MessageBoxHeader,
  MessageBoxText,
  MessageBoxContent,
} from '../style/components/messageBox';

const MessageBox = ({ messageObject, currentUser }) => (
  <MessageBoxContainer>
    <MessageBoxWrapper>
      <MessageBoxForm>
        {messageObject.map((messageItem, index) => {
          const { message, user } = messageItem;
          return (
            <MessageBoxContent key={index} isCurrentUser={user === currentUser}>
              <MessageBoxText>{message}</MessageBoxText>
            </MessageBoxContent>
          );
        })}
      </MessageBoxForm>
      <MessageInput currentUser={currentUser} />
    </MessageBoxWrapper>
  </MessageBoxContainer>
);

MessageBox.propTypes = {
  messageObject: PropTypes.array,
  currentUser: PropTypes.string,
};

export default MessageBox;

As you can see, this component also contains MessageInput, written a moment ago. But how we display the messages? From the server we get an array of objects containing the users name and the message content. We map all this and display it in a MessageBoxText container. No extra magic. We simply render the received data. If you always thought that chat is something complicated, I hope I showed you the easiest way to do it. Remember, there is never anything too difficult, there are only things we do not know how to implement yet.

Congratulations. You have just created your first real-time chat. As I mentioned at the beginning, it's nothing complicated. You just need a few lines of code and understanding how socket.io data exchange works. You can find the full version of the chat on my Github - ChatMe.now. If you feel I helped you, please leave a star!

chat.me now chat app design

Summary

I hope I have helped you learn something about implementing real-time communication using WebSockets. Remember, it's good to try new things and not limit yourself to using only one known way of exchanging data. I have caught myself thinking that an application must have a REST API.

Applications like this one give unlimited possibilities for expansion and stimulate imagination. For me, it was a huge boost and a dose of excitement bringing with it a lot of motivation to keep working. Coding ChatMe.now took me two evenings, I wanted to try something new, and of course, this is not the end of the development of this application. In the future, I would like to add TypeScript (and of course describe it in the next article). Applications like this leave a lot of room for improvement - we can add security, logging, a separate layer with API etc.

Socket.io is a great tool not only for building SPA. As I mentioned earlier we can build a multiplayer game, parcel service for shipping companies, retroboards and much more. I Hope I stimulated your imagination and encouraged you to experiment on your own. Let me know - you can find my social media in my bio!