Logo
Published on

Building a Real-Time Chat App with Next.js, Socket.io, and TypeScript

Authors
  • Name
    Twitter

Creating a real-time chat application involves integrating various technologies to achieve seamless communication between users. In this tutorial, we will walk through the process of building a simple chat app using Next.js, Socket.io, and TypeScript. This combination allows us to create a modern and responsive application with a real-time messaging feature.

Prerequisites

Before we begin, ensure that you have Node.js and npm (Node Package Manager) installed on your machine.

Step 1: Setting Up the Project

To get started, let’s set up the foundation of our project using Next.js with TypeScript. Open your terminal and run the following command:

npx create-next-app chat_app

Follow these options when prompted:

  • Would you like to use Typescript?: Yes
  • Would you like to use EsLint?: No
  • Would you like to use Tailwind CSS?: No
  • Would you like to use the ‘src/’ directory?: Yes
  • Would you like to use App Router?: Yes
  • Would you like to customize the default import alias?: Yes
  • What import alias would you like configured?: Just press Enter (default: @/*)

This will set up your Next.js project with TypeScript and the specified configurations.

Then your NextJs project with Typescript will be created. Now your package.json file will look something like this:

Note: I’m using Nextjs 13.4 version

{
 "name": "chat_app",
 "version": "0.1.0",
 "private": true,
 "scripts": {
 "dev": "next dev",
 "build": "next build",
 "start": "next start",
 "lint": "next lint"
},
 "dependencies": {
 "@types/node": "20.4.8",
 "@types/react": "18.2.18",
 "@types/react-dom": "18.2.7",
 "next": "13.4.13",
 "react": "18.2.0",
 "react-dom": "18.2.0",
 "typescript": "5.1.6"
 }
}

And your folder structure will look something like this:

Step 2: Installing Socket.io

Now that our project is set up, let’s install the necessary dependencies for both the client and server components. In your terminal, navigate to the project directory and run:

npm install socket.io socket.io-client @types/socket.io

Step 3: Setting Up the Server

Create a server.js file in the root of your project. This will be our Socket.io server. Add the following code to set up the server:

const http = require('http')
const { Server } = require('socket.io')
const cors = require('cors')

const httpServer = http.createServer()

const io = new Server(httpServer, {
  cors: {
    origin: 'http://localhost:3000', // Replace with your frontend URL
    methods: ['GET', 'POST'],
    allowedHeaders: ['my-custom-header'],
    credentials: true,
  },
})

io.on('connection', (socket) => {
  console.log('A user connected:', socket.id)
  socket.on('join_room', (roomId) => {
    socket.join(roomId)
    console.log(`user with id-${socket.id} joined room - ${roomId}`)
  })

  socket.on('send_msg', (data) => {
    console.log(data, 'DATA')
    //This will send a message to a specific room ID
    socket.to(data.roomId).emit('receive_msg', data)
  })

  socket.on('disconnect', () => {
    console.log('A user disconnected:', socket.id)
  })
})

const PORT = process.env.PORT || 3001
httpServer.listen(PORT, () => {
  console.log(`Socket.io server is running on port ${PORT}`)
})

And in the package.json file add “node”: ”node server.js” in the scripts part

"scripts":  {
"dev":  "next dev",
"build":  "next build",
"start":  "next start",
"lint":  "next lint",
"node":  "node server.js"
},

Step 4: Creating the Client Components

Let’s create the components for our chat page. Inside the src/components folder, create two files: page.tsx and chat.module.css.

In page.tsx, add the code for the chat page component, including the state management for messages and sending messages.

"use client";
import React, { useEffect, useState } from "react";
import style from "./chat.module.css";

interface IMsgDataTypes {
  roomId: String | number;
  user: String;
  msg: String;
  time: String;
}

const ChatPage = ({ socket, username, roomId }: any) => {
  const [currentMsg, setCurrentMsg] = useState("");
  const [chat, setChat] = useState<IMsgDataTypes[]>([]);

  const sendData = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (currentMsg !== "") {
      const msgData: IMsgDataTypes = {
        roomId,
        user: username,
        msg: currentMsg,
        time:
          new Date(Date.now()).getHours() +
          ":" +
          new Date(Date.now()).getMinutes(),
      };
      await socket.emit("send_msg", msgData);
      setCurrentMsg("");
    }
  };

  useEffect(() => {
    socket.on("receive_msg", (data: IMsgDataTypes) => {
      setChat((pre) => [...pre, data]);
    });
  }, [socket]);

  return (
    <div className={style.chat_div}>
      <div className={style.chat_border}>
        <div style={{ marginBottom: "1rem" }}>
          <p>
            Name: <b>{username}</b> and Room Id: <b>{roomId}</b>
          </p>
        </div>
        <div>
          {chat.map(({ roomId, user, msg, time }, key) => (
            <div
              key={key}
              className={
                user == username
                  ? style.chatProfileRight
                  : style.chatProfileLeft
              }
            >
              <span
                className={style.chatProfileSpan}
                style={{ textAlign: user == username ? "right" : "left" }}
              >
                {user.charAt(0)}
              </span>
              <h3 style={{ textAlign: user == username ? "right" : "left" }}>
                {msg}
              </h3>
            </div>
          ))}
        </div>
        <div>
          <form onSubmit={(e) => sendData(e)}>
            <input
              className={style.chat_input}
              type="text"
              value={currentMsg}
              placeholder="Type your message.."
              onChange={(e) => setCurrentMsg(e.target.value)}
            />
            <button className={style.chat_button}>Send</button>
          </form>
        </div>
      </div>
    </div>
  );
};

export default ChatPage;

And in chat.module.css, add the style for the chat page component.

.chat_div {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.chat_border {
border: 1px solid red;
padding: 5px;
}
.chat_input {
height: 2rem;
width: 15rem;
padding: 5px;
}
.chat_button {
height: 2rem;
}
.chatProfileRight {
display: flex;
align-items: center;
gap: 5px;
flex-direction: row-reverse;
margin-bottom: 5px;
}
.chatProfileLeft {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 5px;
}
.chatProfileSpan {
background-color: rgb(213, 213, 182);
height: 2rem;
width: 2rem;
border-radius: 50%;
border: 1px solid white;
display: flex;
justify-content: center;
align-items: center;
color: black;
}

Step 5: Setting Up the Chat Page

In the src/app directory, modify the page.tsx file to handle user interaction and rendering the chat page.

"use client";
import styles from "./page.module.css";
import { io } from "socket.io-client";
import { useState } from "react";
import ChatPage from "@/components/page";

export default function Home() {
  const [showChat, setShowChat] = useState(false);
  const [userName, setUserName] = useState("");
  const [showSpinner, setShowSpinner] = useState(false);
  const [roomId, setroomId] = useState("");

  var socket: any;
  socket = io("http://localhost:3001");

  const handleJoin = () => {
    if (userName !== "" && roomId !== "") {
      console.log(userName, "userName", roomId, "roomId");
      socket.emit("join_room", roomId);
      setShowSpinner(true);
      // You can remove this setTimeout and add your own logic
      setTimeout(() => {
        setShowChat(true);
        setShowSpinner(false);
      }, 4000);
    } else {
      alert("Please fill in Username and Room Id");
    }
  };

  return (
    <div>
      <div
        className={styles.main_div}
        style={{ display: showChat ? "none" : "" }}
      >
        <input
          className={styles.main_input}
          type="text"
          placeholder="Username"
          onChange={(e) => setUserName(e.target.value)}
          disabled={showSpinner}
        />
        <input
          className={styles.main_input}
          type="text"
          placeholder="room id"
          onChange={(e) => setroomId(e.target.value)}
          disabled={showSpinner}
        />
        <button className={styles.main_button} onClick={() => handleJoin()}>
          {!showSpinner ? (
            "Join"
          ) : (
            <div className={styles.loading_spinner}></div>
          )}
        </button>
      </div>
      <div style={{ display: !showChat ? "none" : "" }}>
        <ChatPage socket={socket} roomId={roomId} username={userName} />
      </div>
    </div>
  );
}

And in page.module.css, add the style for the app page component.

.main_div {
height: 100vh;
widows: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.main_input {
height: 2rem;
width: 15rem;
padding: 5px;
}
.main_button {
height: 2rem;
width: 15rem;
display: flex;
justify-content: center;
align-items: center;
}
.loading_spinner {
border: 4px solid rgba(0, 0, 0, 0.3);
border-top: 4px solid #2196f3;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Step 6: Running the Application

With the server and client components ready, start the development servers:

In Terminal 1 (Backend):

npm run node

In Terminal 2 (Frontend):

npm run dev

Open two browser tabs and navigate to http://localhost:3000.

Now in the left side browser enter the username (i.g. Ravi) and room ID (i.g 123) and similarly, in the right side browser enter the username (i.g. Kumar) and room ID (i.g. 123) and press Enter.

Note: room ID must be the same in both browser tabs

You should see the chat interface and be able to send and receive messages in real-time.