Learn How to implement Real time Chat Application in Nextjs App Router

Derick Zr • August 07, 2023

7 min read

Learn How to implement Real time Chat Application in Nextjs App Router

Introduction

In this tutorial We will be using the power of Websockets to build a real time Chat App. At the end of the tutorial, you will have a basic understanding on how websockets work and how to create communication between your server and your frontend application to send and receive messages in real time.

Demo

demo
cartoon by hackles
# SET UP A CUSTOM WEBSOCKET SERVER

In your root directory, create a new folder and name it wss

Folder structure:

.
├── ./build
├── ./LICENSE
├── ./node_modules
├── ./nodemon.json
├── ./package.json
├── ./package-lock.json
├── ./src
└── ./tsconfig.json

inside src we have:

.
├── ./data
├── ./lib
└── ./types

sub-folder:

.
├── ./data
├── ./data/message.ts
└── ./data/user.ts
├── ./lib
└── ./lib/joinRoom.ts
├── ./server.ts
└── ./types
    └── ./types/index.ts

To see what is inside of each files, the link to repository is here. If all is set correctly, in your terminal go ahead a run the server npm run dev you should see a message saying Server is running on port 3001 now like this:

[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node src/server.ts`
Server is running on port 3001 now!

FRONTEND

 PS: This tutorial assumes you have a basic knowledge of React or Next.js.

The Design process

Here, I'll walk you through creating the required pages for the web application.

First of all, you need a homepage for your application. The homepage should have:

  • Create Room: A section for user to create new chat room and get an access link that they can copy and share to others
  • Join Room: A section where user can join a room if they have an access Key
    demo
    cartoon by hackles
    Next, create a chat-room-page: this page is where people using the same room link will be chatting. A user can see how many people are in the room, get notified who joined and who leaves the room.
    demo
    cartoon by hackles

Since you have learnt how to build the pages of the application. Let's start writing some codes.

Connecting to the server

In order for the frontend to communicate with our server through websocket we need to make the connection by using a library called socket.io For this article I'm using "socket.io version 4.7.1 to install the same version in package.json in dependencies add this line: "socket.io-client": "^4.7.1" then go ahead and close the server then run npm install after the installation finished re-run the server.

Next create a folder at the root of your Next.js called lib inside create socket.ts file and copy the socket configuration for your application:

import { io } from "socket.io-client"
const SERVER =
  process.env.NODE_ENV === "production"
    ? "your-server-domain-name"
    : "http://localhost:3001"
 
export const socket = io(SERVER, { transports: ["websocket"] })

Subscribing to socket events

Now navigate to the homepage directory of your application, and copy this:

import { socket } from "@/lib/sockets";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
...
 
const router = useRouter();
const setUser = useUserStore((state) => state.setUser);
 
const setMembers = useMembersStore((state) => state.setMembers);
 
useEffect(() => {
 
socket.on("room-joined", ({ user, roomId, members }: RoomJoinedData) => {
 
window.localStorage.setItem("admin", user.id);
setUser(user);
 
setMembers(members);
 
router.replace(`/${roomId}`);
});
function handleErrorMessage({ message }: { message: string }) {
console.log("Failed to join");
}
socket.on("room-not-found", handleErrorMessage);
socket.on("invalid-data", handleErrorMessage);
return () => {
socket.off("room-joined");
socket.off("room-not-found");
socket.off("invalid-data", handleErrorMessage);
};
 
}, [router, setUser, setMembers]);
 

This code first defines a function called handleRoomJoined that is called when the room-joined event is emitted by the socket. The handleRoomJoined function takes a RoomJoinedData object as its argument, which contains information about the user who joined the room, the room ID, and the list of members in the room. The handleRoomJoined function then sets the user's state, the members' state, and the router's location to reflect the fact that the user has joined the room.

At this point I created two separated components to handle the join and create boxes.

Create Room Component:

import { socket } from "@/lib/sockets";
import { nanoid } from "nanoid";
import React, { FormEvent, useEffect, useState } from "react";
 
...
const [userName, setUserName] = useState("");
const [error, setError] = useState<string | null>(null);
const [roomId, setRoomId] = useState<string>("");
 
//Generating the Room-ID
useEffect(() => {
setRoomId(nanoid());
}, []);
 
const createRoom = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
 
if (!roomId.trim() || !userName.trim())
return setError("You need to fill in your Name.");
 
socket.emit("create-room", { roomId, username: userName });
};
 

Here we check if the roomId and userName fields are empty. If they are empty, we return "You need to fill in your Name." Otherwise, the function emits the create-room event on the socket. The create-room event takes a roomId and username as its arguments. The roomId is the ID of the new room, and the username is the username of the user who is creating the room.

Join Room Component:

const joinRoom = (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  setError(null)
  if (!roomId.trim() || !userName.trim())
    return setError("You need to fill in all fields.")
  socket.emit("join-room", { roomId, username: userName })
}

Same as the code for create-room, Here we are checking if the roomId and userName fields are empty. If they are empty, an error state "You need to fill in all fields." Otherwise, the function emits the join-room event on the socket.

The socket.emit method is used to emit an event on the socket. The join-room event is the name of the event that is emitted. The { roomId, username: userName } object is the data that is emitted with the event. The roomId is the ID of the room that the user wants to join, and the username is the username of the user who is joining the room.

Chat Room page

const [messageText, setMessageText] = useState("")
const [messagesList, setMessagesList] = useState<MessageObjData[]>([])
 
const lastItemRef = useRef<HTMLLIElement | null>(null)
 
const [getAdmin, setGetAdmin] = useState<string | null>("")
const [messages, setMessages] = useMessageStore((state) => [
  state.message,
  state.setMessage,
])
 
const messageList = useCallback((message: MessageObjData) => {
  setMessagesList((prev) => [...prev, message])
}, [])
 
const roomId = usePathname().slice(1)
useEffect(() => {
  setGetAdmin(localStorage.getItem("admin"))
}, [])
useEffect(() => {
  socket.on("receive-message", messageList)
  return () => {
    socket.off("receive-message")
  }
}, [messageList, messages, setMessages])
 
useEffect(() => {
  if (messagesList.length === 0) return
  lastItemRef.current?.scrollIntoView({ behavior: "smooth" })
}, [messagesList])
 
function onSubmit() {
  setMessageText("")
  socket.emit("send-message", {
    roomId: roomId,
    data: messageText,
    type: "text",
  })
}

The messageList function is a callback function that is used to add a new message to the messagesList variable. The useEffect function is used to listen for the receive-message event on the socket. The onSubmit function is used to send a message to the chat room.

then uses the useEffect hook to listen for the receive-message event on the socket. When the receive-message event is emitted, the function calls the messageList function to add the new message to the messagesList variable.

Leaving the Room Create a leave the room button to let the user leave the room if they need to. still in the chat-room , copy this code:

useEffect(() => {
  socket.on("update-members", (members) => {
    setMembers(members)
  })
 
  socket.on("send-notification", ({ title, message }: Notification) => {
    alert(`${title}, ${message}`)
  })
 
  return () => {
    socket.off("update-members")
    socket.off("send-notification")
  }
}, [setMembers])
 
const leaveRoom = () => {
  socket.emit("leave-room")
  router.replace("/")
}

The sendNotification function is called when the send-notification event is emitted.

The hook then uses the useEffect hook to listen for the update-members and send-notification events. When either event is emitted, the corresponding function is called.

The hook also defines a function called leaveRoom. The leaveRoom function is called when the user clicks the "Leave Room" button. The leaveRoom function emits the leave-room event on the socket and then redirects the user to the home page.