WebSocket with React, Nodejs, and Docker: Building a Chat Application
Tags: javascript, docker, react, node
Published: 2022-04-24 17:20:42

Websockets is a great technology if you are looking to build reactive or event-driven applications. Most of the time, this is the same technology used by instantaneous messaging products.

In this article, we'll build a chat application using React and Node. At the end of this article, there is an optional part ( but very useful ) on how to wrap the whole project into Docker.🚀

## Demo Project

Here's a demo of what we'll be building.


![Demo](https://cdn.hashnode.com/res/hashnode/image/upload/v1650820565638/eQ7oen5Rw.png)

## Setup project

First of all, create a simple React project.

```shell
yarn create react-app react-chat-room
```

Once the project is created, make sure everything works by running the project.

```shell
cd react-chat-room
yarn start
```

And you'll have something similar running at <http://localhost:3000>.

![Started React application](https://cdn.hashnode.com/res/hashnode/image/upload/v1649575625302/8wYFebD1Y.png)

After that, let's set up the Node server. Inside the project root, create a directory called server.

Inside this directory, create an `index.js` file and a `package.json` file too. 

Here's the content of the `package.json` file.

```json
{
    "private": true,
    "name": "websocket-chat-room-server",
    "description": "A React chat room application, powered by WebSocket",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
        "start": "node ."
    },
    "dependencies": {
        "ws": "^8.5.0"
    }
}
```

And inside the `index.js` file, add this basic configuration. We are just starting the `ws` server to make sure everything is working.

```js
const WebSocket = require('ws');

const server = new WebSocket.Server({
        port: 8080
    },
    () => {
        console.log('Server started on port 8080');
    }
);
```

After that, run the following command to make sure the server is running.

```shell
yarn start
```

## Writing the chat feature on the server-side

The Node server handles all requests sent via WebSockets. Let's build a simple backend feature to notify all chat users about messages.
Here's how it'll go:

- The user opens a connection and joins a room.
- Once he has joined the room, he can send a message.
- The message is received by the server and passes some validation checks.
- Once the message is validated, the server notifies all users in the chat room about the message.

First of all, let's create a set of users and also a function to send a message.

```js
...
const users = new Set();

function sendMessage (message) {
    users.forEach((user) => {
        user.ws.send(JSON.stringify(message));
    });
}
```

With these basics function ready, let's write the basics interactions `ws` methods to handle message events, connection events, and close events.

```js
server.on('connection', (ws) => {
    const userRef = {
        ws,
    };
    users.add(userRef);

    ws.on('message', (message) => {
        console.log(message);
        try {

            // Parsing the message
            const data = JSON.parse(message);

            // Checking if the message is a valid one

            if (
                typeof data.sender !== 'string' ||
                typeof data.body !== 'string'
            ) {
                console.error('Invalid message');
                return;
            }

            // Sending the message

            const messageToSend = {
                sender: data.sender,
                body: data.body,
                sentAt: Date.now()
            }

            sendMessage(messageToSend);

        } catch (e) {
            console.error('Error passing message!', e)
        }
    });

    ws.on('close', (code, reason) => {
        users.delete(userRef);
        console.log(`Connection closed: ${code} ${reason}!`);
    });
});
```

Well, the WebSocket server is working. We can now move the UI of the chat application with React.

## Writing the chat application with React

The React application will have the following workflow: 

- The user is redirected by default to a page where he enters a username.
- After entering the username, the user is redirected to the chat room and can start talking with other online members.

Let's start by installing the needed packages such as react-router for routing in the application and tailwind for styling.

```shell
yarn add react-router-dom tailwindcss
```

Next, we need to create a configuration file for tailwind.
Use `npx tailwindcss-cli@latest init` to generate `tailwind.config.js` file containing the minimal configuration for tailwind.

```json
module.exports = {
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};
```

The last step will be to include tailwind in the `index.css` file.

```css
/*src/index.css*/

@tailwind base;
@tailwind components;
@tailwind utilities;
```

After that, create the `src/components` directory and add a new file named `Layout.jsx`. This file will contain a basic layout for the application so we can avoid **DRY**.

```jsx
import React from "react";

function Layout({ children }) {
  return (
    <div className="w-full h-screen flex flex-col justify-center items-center space-y-6">
      <h2 className="text-3xl font-bold">React Ws Chat</h2>
      {children}
    </div>
  );
}

export default Layout;
```

In the same directory, create a file called `SendIcon.js` and add the following content.

```jsx
const sendIcon = (
  <svg
    width="20"
    height="20"
    viewBox="0 0 20 20"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      d="M19 10L1 1L5 10L1 19L19 10Z"
      stroke="black"
      strokeWidth="2"
      strokeLinejoin="round"
    />
  </svg>
);

export default sendIcon;

```

### Writing the authentication page

Inside the `src/pages`, create a new file called `LoginPage.jsx`. Once it's done, let's add the JavaScript logic to handle the form submission.

```jsx
import React from "react";
import { useNavigate } from "react-router-dom";
import Layout from "../components/Layout";

function LoginPage() {

  const navigate = useNavigate();
  
  const [username, setUsername] = React.useState("");

  function handleSubmit () {
    if (username) {
        navigate(`/chat/${username}`);
    }
  }
  
  return (
      <Layout>
      // Form here
      </Layout>
  )
}

export default LoginPage;
```

And finally here's the JSX.

```jsx
...
  return (
    <Layout>
      <form class="w-full max-w-sm flex flex-col space-y-6">
        <div class="flex flex-col items-center mb-6 space-y-6">
          <label
            class="block text-gray-500 font-bold md:text-right mb-1 md:mb-0 pr-4"
            for="username"
          >
            Type the username you'll use in the chat
          </label>
          <input
            class="bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500"
            id="username"
            type="text"
            placeholder="Your name or nickname"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            required
          />
        </div>
        <div class="md:flex md:items-center">
          <div class="md:w-1/3"></div>
          <div class="md:w-2/3">
            <button
              class="self-center shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded"
              type="button"
              onClick={handleSubmit}
            >
              Log in the chat
            </button>
          </div>
        </div>
      </form>
    </Layout>
  );
  ...
```

Let's explain what we are doing here:

- We are defining the state and functions needed to submit the form and move to the chat room.

- We also make sure that the `username` value is not empty.

Nice, let's move to the next step, the hottest part of this project.

### Writing the Chat room component

Inside the `src/pages`, create a file called `ChatPage.jsx`. This file will contain all the logic and UI for the Chat room feature.
Before going into coding it, let's talk about how the WebSocket connection is handled here.

- Once the user is redirected to the `ChatPage.jsx` page, a `ws` connection is initiated.  
- If the user enters and sends a message, an event of type `message` is sent to the server.
- Every time another user is sending a message, an event is sent to the React application and we update the list of messages shown on the screen.

Let's write the `js` logic to handle this first.

```jsx
import React, { useRef } from "react";
import Layout from "../components/Layout";
import { useParams } from "react-router-dom";
import { sendIcon } from "../components/SendIcon"

function ChatPage() {
  const [messages, setMessages] = React.useState([]);
  const [isConnectionOpen, setConnectionOpen] = React.useState(false);
  const [messageBody, setMessageBody] = React.useState("");

  const { username } = useParams();

  const ws = useRef();

  // sending message function

  const sendMessage = () => {
    if (messageBody) {
      ws.current.send(
        JSON.stringify({
          sender: username,
          body: messageBody,
        })
      );
      setMessageBody("");
    }
  };

  React.useEffect(() => {
    ws.current = new WebSocket("ws://localhost:8080");

    // Opening the ws connection

    ws.current.onopen = () => {
      console.log("Connection opened");
      setConnectionOpen(true);
    };

    // Listening on ws new added messages

    ws.current.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages((_messages) => [..._messages, data]);
    };

    return () => {
      console.log("Cleaning up...");
      ws.current.close();
    };
  }, []);

  const scrollTarget = useRef(null);

  React.useEffect(() => {
    if (scrollTarget.current) {
      scrollTarget.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages.length]);

  return (
    <Layout>
      // Code going here
    </Layout>
  );
}

export default ChatPage;
```

Let's add the UI for the list of messages first.

```jsx
...
      <div id="chat-view-container" className="flex flex-col w-1/3">
        {messages.map((message, index) => (
          <div key={index} className={`my-3 rounded py-3 w-1/3 text-white ${
            message.sender === username ? "self-end bg-purple-600" : "bg-blue-600"
          }`}>
            <div className="flex items-center">
              <div className="ml-2">
                <div className="flex flex-row">
                  <div className="text-sm font-medium leading-5 text-gray-900">
                    {message.sender} at
                  </div>
                  <div className="ml-1">
                    <div className="text-sm font-bold leading-5 text-gray-900">
                      {new Date(message.sentAt).toLocaleTimeString(undefined, {
                        timeStyle: "short",
                      })}{" "}
                    </div>
                  </div>
                </div>
                <div className="mt-1 text-sm font-semibold leading-5">
                  {message.body}
                </div>
              </div>
            </div>
          </div>
        ))}
        <div ref={scrollTarget} />
      </div>
```

The messages from the user will be in purple and the messages from other users will be in blue.

Next step, let's add a small input to enter a message and send it.

```jsx
...
      <footer className="w-1/3">
        <p>
          You are chatting as <span className="font-bold">{username}</span>
        </p>
        <div className="flex flex-row">
          <input
            id="message"
            type="text"
            className="w-full border-2 border-gray-200 focus:outline-none rounded-md p-2 hover:border-purple-400"
            placeholder="Type your message here..."
            value={messageBody}
            onChange={(e) => setMessageBody(e.target.value)}
            required
          />
          <button
            aria-label="Send"
            onClick={sendMessage}
            className="m-3"
            disabled={!isConnectionOpen}
          >
            {sendIcon}
          </button>
        </div>
      </footer>
```

Here's the final code for the `ChatPage` component.

```jsx
import React, { useRef } from "react";
import Layout from "../components/Layout";
import { useParams } from "react-router-dom";
import { sendIcon } from "../components/SendIcon"

function ChatPage() {
  const [messages, setMessages] = React.useState([]);
  const [isConnectionOpen, setConnectionOpen] = React.useState(false);
  const [messageBody, setMessageBody] = React.useState("");

  const { username } = useParams();

  const ws = useRef();

  // sending message function

  const sendMessage = () => {
    if (messageBody) {
      ws.current.send(
        JSON.stringify({
          sender: username,
          body: messageBody,
        })
      );
      setMessageBody("");
    }
  };

  React.useEffect(() => {
    ws.current = new WebSocket("ws://localhost:8080");

    ws.current.onopen = () => {
      console.log("Connection opened");
      setConnectionOpen(true);
    };

    ws.current.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages((_messages) => [..._messages, data]);
    };

    return () => {
      console.log("Cleaning up...");
      ws.current.close();
    };
  }, []);

  const scrollTarget = useRef(null);

  React.useEffect(() => {
    if (scrollTarget.current) {
      scrollTarget.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages.length]);

  return (
    <Layout>
      <div id="chat-view-container" className="flex flex-col w-1/3">
        {messages.map((message, index) => (
          <div key={index} className={`my-3 rounded py-3 w-1/3 text-white ${
            message.sender === username ? "self-end bg-purple-600" : "bg-blue-600"
          }`}>
            <div className="flex items-center">
              <div className="ml-2">
                <div className="flex flex-row">
                  <div className="text-sm font-medium leading-5 text-gray-900">
                    {message.sender} at
                  </div>
                  <div className="ml-1">
                    <div className="text-sm font-bold leading-5 text-gray-900">
                      {new Date(message.sentAt).toLocaleTimeString(undefined, {
                        timeStyle: "short",
                      })}{" "}
                    </div>
                  </div>
                </div>
                <div className="mt-1 text-sm font-semibold leading-5">
                  {message.body}
                </div>
              </div>
            </div>
          </div>
        ))}
        <div ref={scrollTarget} />
      </div>
      <footer className="w-1/3">
        <p>
          You are chatting as <span className="font-bold">{username}</span>
        </p>

        <div className="flex flex-row">
          <input
            id="message"
            type="text"
            className="w-full border-2 border-gray-200 focus:outline-none rounded-md p-2 hover:border-purple-400"
            placeholder="Type your message here..."
            value={messageBody}
            onChange={(e) => setMessageBody(e.target.value)}
            required
          />
          <button
            aria-label="Send"
            onClick={sendMessage}
            className="m-3"
            disabled={!isConnectionOpen}
          >
            {sendIcon}
          </button>
        </div>
      </footer>
    </Layout>
  );
}

export default ChatPage;
```

Great! Let's move to register the routes.

### Adding routes

Inside the `App.js` file, add the following content.

```jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { LoginPage, ChatPage } from "./pages";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<LoginPage />} />
        <Route path="/chat/:username" element={<ChatPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
```

After that make sure your application is running and you can start testing.


## Dockerizing the application

It's great to have many servers running in this project but it requires quite a lot of setup. What if you are looking to deploy it for example? It can be quite complicated.

[Docker](https://www.docker.com/)  is an open platform for developing, shipping, and running applications inside containers.
Why use Docker?
It helps you separate your applications from your infrastructure and helps in delivering code faster.

If it's your first time working with Docker, I highly recommend you go through a quick tutorial and read some documentation about it.

Here are some great resources that helped me:

- [Docker Tutorial](https://www.youtube.com/watch?v=eN_O4zd4D9o&list=PLPoSdR46FgI5wOJuzcPQCNqS37t39zKkg)
- [Docker curriculum](https://docker-curriculum.com/)

Firstly, add a `Dockerfile` at the root of the project. This `Dockerfile` will handle the React server. 

```docker
FROM node:16-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
```

After that, add also a `Dockerfile` in the `server` directory.

```docker
FROM node:16-alpine

WORKDIR /app/server

COPY package.json ./server

COPY yarn.lock ./server

RUN yarn install --frozen-lockfile

COPY . .
```

And finally, at the root of the project, add a `docker-compose.yaml` file.

```yaml
version: "3.8"
services:
  ws:
    container_name: ws_server
    restart: on-failure
    build:
      context: .
      dockerfile: server/Dockerfile
    volumes:
      - ./server:/app/server
    ports:
      - "8080:8080"
    command: >
      sh -c "node ."

  react-app:
    container_name: react_app
    restart: on-failure
    build: .
    volumes:
      - ./src:/app/src
    ports:
      - "3000:3000"
    command: >
      sh -c "yarn start"
    depends_on:
      - ws
```

Once it's done, run the containers with the following command.

```shell
docker-compose up -d --build
```

The application will be running at the usual port.

And voilà! We've successfully dockerized our chat application.🚀

## Conclusion

In this article,  we've learned how to build a chat application using React, Node, and Docker.

And as every article can be made better so your suggestion or questions are welcome in the comment section. 😉

Check the code of this tutorial  [here](https://github.com/koladev32/react-websocket-docker-chat-room).


*Article posted using [bloggu.io](https://bloggu.io). Try it for free.*