DEV Community

Cover image for How to Build a Realtime Group Chat Application with React and Socket.io
Fredrick Emmanuel
Fredrick Emmanuel

Posted on • Updated on

How to Build a Realtime Group Chat Application with React and Socket.io

This article explain what the framework "Socket.io" means and Create a simple Group Chat App with React. Here is the link to the GitHub repository. Feel free to comment on any issue, I will always be available to reply.

Goals

The Aim of this tutorial is to explain how Socket.io V4 works and simplify the use of it with a front-end framework like React

Table of Content

  • Prerequisites
  • Getting Started
  • Setting up the Server
  • Setting up React
  • Connect Client to Server
    • Creating the Server connection
    • Refactoring React App
    • Creating Routes
    • Connecting the React to the Server
  • Handling CORS
  • Connecting to Different Rooms
  • Messaging
    • Welcome Message
    • Sending Message
  • Disconnect
  • Conclusion

Prerequisites

What is Socket.io?

Socket.io is a JavaScript Library that allows bi-directional Secured Realtime communication between the browser and the server. Which means that if a user sends a data, the recipient(s) of that data would receive immediately, depending on the internet speed.

How it Works

According to Socket.io, The client will try to establish a WebSocket connection if possible, and will fall back on HTTP long polling if not. WebSocket Establishes the connection between the client and the server. Socket.io makes use of this connection the WebSocket brings to transfer data.

Let's Jump deep into the course of this article.

Getting Started

Open up the terminal in your desired folder, then Create a new folder and move into it:

mkdir react-chat-app
cd react-chat-app
npx create-react-app .
Enter fullscreen mode Exit fullscreen mode

Navigate back to the projects root folder, initialize the project and install server dependencies:

npm init -y
npm i express socket.io concurrently nodemon
Enter fullscreen mode Exit fullscreen mode

Concurrently helps in running more than command at the same time without creating another terminal. This would really help in running both our react and server side together in one terminal.

Nodemon is a tool that automatically restarts the server when changes are made to the file directory.

Setting up the Server

After all installations are complete, we create a server.js file in the projects root directory and require all necessary dependency:

const http = require("http");
const express = require("express");
Enter fullscreen mode Exit fullscreen mode

Setting up our server for socket.io would not be the same as our normal express setup. According to socket.io documentation, we create our set up socket.io using node http server:

const app = express()
const server = http.createServer(app)
const io = socketio(server)

const PORT = process.env.PORT || 5000

server.listen(PORT, () => console.log(`Server is Quannected to Port ${PORT}`))
Enter fullscreen mode Exit fullscreen mode

The constant PORT makes use of ES modules that checks if our app is deployed. If the app is not deployed, it would return 5000.

We need to add few lines of code to our script tag inside the package.json file, to enable us run our server using npm:

    "start": "node server.js",
    "server": "nodemon server",
    "dev": "concurrently \"npm run server\" \"cd client && npm start\""
Enter fullscreen mode Exit fullscreen mode

Let's try out our app in our terminal:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Setting Up React

Move into react-chat-app and let's open up our terminal to install the dependencies we would be utilizing in this article:

npm i react-router socket.io-client query-string react-router-dom
Enter fullscreen mode Exit fullscreen mode

Socket.io-client is a dependency created by socket.io to help connect to socket.io in the server.

Query-string helps us get parameter in our url from the address bar.

Connect Client to Server

This is where the our messaging app starts. Here, we would create a socket.io connection between our react app with our server app.

Creating the Server Connection

A listening event has to be made in the server.js for client to connect to the server:

io.on("connection", (socket) => {
    console.log('A Connection has been made')
    socket.on('disconnect', ()=> {
        console.log('A disconnection has been made')
    })
})
Enter fullscreen mode Exit fullscreen mode

The constant io is listening for a connection from the client and when that connection made is, it creates a special socket for that particular connection. The socket, which is passed as a parameter in the arrow function, holds the properties of the connection which has just been made. In our Code, the socket ,which is the connection, listens for when it has been disconnected. And then socket is been removed since there has been a disconnection.

Refactoring React App

Before we can connect to the server, there are some refactoring we need to do to our new React app.

First we need to delete some of the pre-created file in our React app. Delete every thing in the src folder and create index.js in that same src folder. Add the following code to the index.js:

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

To prevent react from yelling at us, we need to create the App.js in the same directory with the index.js. We need to add a functional component to our App.js that would return a simple welcome message:

import React from "react";

const App = () => {
    <h1>App Successfully rendered.</h1>
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Creating Routes

Let's create a folder named components in the src, this would contain all our different component in our React app. In that components folder, create a Home.js and a Chat.js file. When all has been created, navigate back to app.js to set up our routes:

import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Home from "./components/Home";
import Chat from "./components/Chat";

const App = () => (
  <Router>
    <Route path="/" exact component={Home}/>
    <Route path="/chat" component={Chat} />
  </Router>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

For clarity, theHome and Chat file is in the components and the components is in the src. The components has a s at the the end the word,take notes so you don't encounter an error 💗.

We created a route, that makes use of the functional component Homewhen accessing the homepage and the Chat when accessing the Chat page.

The Home component would contain a form that would redirect us to the Chat component of the specified group. Open up the Home.js file and set up our form:

import React, { useState } from "react";
import { Link } from "react-router-dom";

const Home = () => {
  const [name, setName] = useState("");
  const [room, setRoom] = useState("");

  return (
      <div>
        <h1>Home Page</h1>
        <div>
          <input
            placeholder="Name"
            type="text"
            onChange={(event) => setName(event.target.value)}
            required
          />
        </div>
        <div>
          <input
            placeholder="Room"
            type="text"
            onChange={(event) => setRoom(event.target.value)}
            required
          />
        </div>
        <Link
          onClick={(e) => (!name || !room ? e.preventDefault() : null)}
          to={`/chat?name=${name}&room=${room}`}
        >
          <button type="submit">
            Sign In
          </button>
        </Link>
      </div>
  );
};

export default Home;

Enter fullscreen mode Exit fullscreen mode

To keep this article as short as possible, it would not be containing any style. You can add custom style if you prefer.

We imported useState to hold the user's inputted name and room in a State value. Read more on useState.

In all the input tags, we had an onChange event that listens for a change in input value and save it in the state. We made use of the Link, imported from react-router-dom, to redirect us to the Chat-page (passing name and room as a parameter) if and only if our name and room State Variable has a value.

Connecting the React to the Server

We have set up our form, the next step is to create a connect and a disconnect from the server in our chat.js:

import React, { useState, useEffect } from "react";
import queryString from "query-string";
import io from "socket.io-client";

let socket;

const Chat = ({ location }) => {
  const [name, setName] = useState("");
  const [room, setRoom] = useState("");
  const ENDPOINT = "http://localhost:5000";

  useEffect(() => {
    const { name, room } = queryString.parse(location.search);
    socket = io(ENDPOINT);
    setRoom(room);
    setName(name);
  }, [location.search]);

  return <div>Chat</div>;
};

export default Chat;

Enter fullscreen mode Exit fullscreen mode

The App.js file passed down a prop to Chat.js,location , using react-router-dom and this location prop contains the url. Then we got the parameters (name and room) from the url using the query-string dependency and set them to a State Variable. The useEffect runs every time location.search changes value. Read more on useEffect .

Handling CORS

In the useEffect block, we created an instance of socket and passed in our Server's Endpoint http://localhost:5000. This would cause a breach in the Cross Origin Resource Sharing CORS Policy since we are trying to data between two different routes.
CORS ERROR![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9cog93uj3zr50vz7dlhi.png)<br>

Since Socket.io V3, we need to explicitly enable CORS in our server to ensure the client successfully connect to the server.

Don't Panic🙂, We need to create options in the server.js Socket.io connection to permit the connection from the client. Since we have already declared the constant io, we just need to add the options to the connection:

const io = require("socket.io")(server, {
  cors: {
    origin: "http://localhost:3000",
    methods: ["GET", "POST"],
    allowedHeaders: ["my-custom-header"],
    credentials: true,
  },
});
Enter fullscreen mode Exit fullscreen mode

Connecting to Different Rooms

We have to create a receiver in the server, waiting to receive new connection from the client. Create a new file user.js, in the same directory with our server's file,that would be in charge of managing our users:

let users = [];

exports.addUser = ({ id, name, room }) => {
  if (!name || !room) return { error: "name and room required." };
  const user = { id, name, room };

  users.push(user);

  return { user };
};
Enter fullscreen mode Exit fullscreen mode

The users variable would contain all the users connected. We returned error if name or room is blank, else we would add the user to the array users and return the user.

We have to Create a listening event for client to join different room in our server.js:

const {addUser} = require('./user')
io.on("connection", (socket) => {

  socket.on("join", ({ name, room }, callBack) => {
    const { user, error } = addUser({ id: socket.id, name, room });

    if (error) return callBack(error);

    socket.join(user.room);
    callBack(null);
  });
//The rest of the code
Enter fullscreen mode Exit fullscreen mode

socket.on listens for any connection from our client with the name "join" then expects name and room as a parameter from the client. The callback sends an error if any or it would just return null, *There must return * from the server.

We need to connect to the event join from the client and emit the inputted name and room as a parameter to the server.

useEffect(() => {
 // The rest of the code 
  socket.emit("join", { name, room }, (error) => {
    if (error) alert(error);
  });
}, [location.search]);
Enter fullscreen mode Exit fullscreen mode

Messaging

Alright, Here we are😮.

Welcome Message

We have to emit a welcome message to the user when the user joins a room.

All our messages would be coming from the server. When the user sends a message, we have to first send that message to the server then send it back to the client. The server would emit the message and the client would receive it.

Navigate to chat.js to create the connection:

const [messages, setMessages] = useState([]);
useEffect(() => {
  socket.on("message", (message) => {
    setMessages((messages) => [...messages, message]);
  });
}, []);
Enter fullscreen mode Exit fullscreen mode

We created another useEffect that receives all messages from the server and set them to messages state variable.
We need to render the messages on for the user in the return block. We need to use JSX to render all messages to the user:

return (
  <div>
    {messages.map((val, i) => {
      return (
        <div key={i}>
          {val.text}
          <br />
          {val.user}
        </div>
      );
    })}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

We mapped through the messages state variable, we specified the key as the index to avoid react error and we returned the text and user passed down from our server.
Let's Connect to the connection created by our client from our server.js:

io.on("connection", (socket) => {
  socket.on("join", ({ name, room }, callBack) => {

    //The rest of the code

    socket.emit("message", {
      user: "Admin",
      text: `Welocome to ${user.room}`,
    });

    // The rest of the code
Enter fullscreen mode Exit fullscreen mode

We are emitting to message connection and we are passing the user and the text as parameters.

We also have to tell other users in the group that a new user has joined. Navigate to server.js:

socket.broadcast
  .to(user.room)
  .emit("message", { user: "Admin", text: `${user.name} has joined!` });
Enter fullscreen mode Exit fullscreen mode

The client is always listening for an emit to message. The message is like the name or an identification for the connection.
The code we just wrote is broadcasting to other users in the room, telling them that a new user has just Joined the Group.

Sending Message

This is how sending of messages would be, We will get the message input from the user, send it to the server and then the server emits that message to everyone in the Group. Let's Open our chat.js and create the input field:

const handleSubmit = (e) => {
  e.preventDefault();
  if (message) {
    socket.emit("sendMessage", { message });
    setMessage("");
  } else alert("empty input");
};

return (
  <div>

    // The rest of the code

    <form action="" onSubmit={handleSubmit}>
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <input type="submit" />
    </form>
  </div>
  );
Enter fullscreen mode Exit fullscreen mode

We are emitting to a new socket event. It takes the message from the user and sends it to the newly created socket event sendMessage. After we've emitted the message to the server using sendMessageevent, Open your server.js and let's create the connection for the sendMessage event:

socket.on("join", ({ name, room }, callBack) => {

  //The rest of the code

  socket.on("sendMessage", ({ message }) => {
    io.to(user.room).emit("message", {
      user: user.name,
      text: message,
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

After we got the message from the client, we emitted that received message to everyone in the group.

Disconnect

This is last part of this article. After the user is done with chatting and would love to disconnect, we would have send a message to every one in the group, informing them that a user has just disconnected. Let's open our user.js file and create a function that would be in charge of removing users from the array:

exports.removeUser = (id) => {
  const index = users.findIndex((user) => user.id === id);
  return users[index];
};

Enter fullscreen mode Exit fullscreen mode

The function removeUser would request for an id, find a user with that id and then returns that user.
We have to import removeUser in our server.js and emit a disconnection message to every one in the returned user group:

const { addUser, removeUser } = require("./user");
io.on("connection", (socket) => {
    // The rest of the code

    socket.on("disconnect", () => {
    const user = removeUser(socket.id);
    console.log(user);
    io.to(user.room).emit("message", {
      user: "Admin",
      text: `${user.name} just left the room`,
    });
    console.log("A disconnection has been made");
  });
 });
Enter fullscreen mode Exit fullscreen mode

When you restart your server, you might get an error when you refresh the Chat page. The best thing to do is to login again from the Home page

Conclusion

Congratulations, We've Successfully Created a Realtime Chat App with React and Socket.io.

Here is the link to the GitHub repository. I almost forgot to Shout out to one of my best YouTubers He was a great help in this article.

Thanks for staying with me till the end💗. If you like to deploy this Chat app to Heroku, I have an article where I talked about Deploying React and Node app to Heroku.
Till We Cross Path, I remain, Fredrick Emmanuel (divofred)😁😁❤❤

Top comments (9)

Collapse
 
rowjay007 profile image
rowjay007

Why did you give credit to the original author of this project from youtube.

Collapse
 
mohnish777 profile image
mohnish777

can u share me the link of that video

Collapse
 
divofred profile image
Fredrick Emmanuel

I added it in the conclusion section.
Video

Collapse
 
mohnish777 profile image
mohnish777

why there is nothing in react folder on github?😤

Collapse
 
divofred profile image
Fredrick Emmanuel

I'm so sorry. I would push the react folder again.
Thanks

Collapse
 
divofred profile image
Fredrick Emmanuel

The react folder should be filled now 😁😁

Collapse
 
abodactyl profile image
Abby Redwood

This is great: easy for beginners to understand and full of content. Thanks for this!

Collapse
 
divofred profile image
Fredrick Emmanuel

No problem

Collapse
 
prachetshah profile image
Prachet • Edited

Really liked the explanation, great post @divofred