Realtime chat app using React, Nestjs and Socket.io

In this article, we will create a real time chat app using React, Socket.io and Nest Js (node js framework) with tailwind CSS.
Technology Stack:
1) React JS - It is a Popular JavaScript library for developing Single page applications. Here, we are using react js for developing client-side application.
2) Socket.io - It is a library that enables low-latency, bidirectional and event-based communication between a client and a server.
3) Nest Js - A progressive Node.js framework for building efficient, reliable and scalable server-side applications.
4) Tailwind CSS - A utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup.
Client - Side Setup
create a new react app using the module create-react-app
npx create-react-app client && cd client
install the following packages
npm i socket.io-client react-scroll-to-bottom
npm install -D tailwindcss
initalize the tailwind css using the below command
npx tailwindcss init
create and configure tailwind.config.js file in the root directory of the client app
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
Add the Tailwind directives to the src/index.css file.
@tailwind base;
@tailwind components;
@tailwind utilities;
edit the App.js file
import React, { useState } from "react";
import io from "socket.io-client";
import ChatWidget from "./component/ChatWidget";
import "./App.css";
const socket = io.connect("http://localhost:3001/chat");
function App() {
const [name, setName] = useState("");
const [room, setRoom] = useState("");
const [showChat, setShowChat] = useState(false);
const joinRoom = () => {
if (name !== "" && room !== "") {
socket.emit("joinRoom", room);
setShowChat(true);
}
};
return (
<div className="flex justify-center align-middle min-w-screen min-h-screen bg-slate-50 h-1">
{!showChat ? (
<div className="flex h-96 lg:w-1/4 sm:w-2/4 mx-auto my-auto bg-white rounded-xl shadow-lg py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full">
<h1 className="text-xl font-medium text-black text-center">
Join A Chat
</h1>
<div className="flex flex-col ">
<label className="text-xl mt-3">Name</label>
<input
type="text"
placeholder="Enter your name"
onChange={(event) => {
setName(event.target.value);
}}
className="p-2 mt-3 border border-indigo-600"
/>
<label className="text-xl mt-3">Room ID</label>
<input
type="text"
placeholder="Room ID..."
onChange={(event) => {
setRoom(event.target.value);
}}
className="p-2 mt-3 border border-indigo-600"
/>
<button
className="mt-3 block bg-indigo-600 text-xl font-bold text-white p-2"
onClick={joinRoom}
>
Join
</button>
</div>
</div>
</div>
) : (
<ChatWidget socket={socket} username={name} room={room} />
)}
</div>
);
}
export default App;
Add chat screen code component in a file src/component/ChatWidget.js
import React, { useEffect, useState } from "react";
import ScrollToBottom from "react-scroll-to-bottom";
function ChatWidget({ socket, username, room }) {
const [currentMessage, setCurrentMessage] = useState("");
const [messageList, setMessageList] = useState([]);
const sendMessage = async () => {
if (currentMessage !== "") {
const messageData = {
sender: username,
room: room,
message: currentMessage,
time:
new Date(Date.now()).getHours() +
":" +
new Date(Date.now()).getMinutes(),
};
await socket.emit("chatToServer", messageData);
// setMessageList((list) => [...list, messageData]);
setCurrentMessage("");
}
};
useEffect(() => {
socket.on("chatToClient", (data) => {
console.log(data);
setMessageList((list) => [...list, data]);
});
}, [socket]);
return (
<div className="h-2/3 lg:w-2/4 sm:w-2/4 mx-auto my-auto bg-white rounded-xl shadow-lg">
<div className="h-12 px-10 rounded-md bg-black relative cursor-pointer">
<p className="block py-2 text-white leading-10">Live Chat</p>
</div>
<div className="h-5/6 border border-black bg-white relative">
<ScrollToBottom className="h-full w-full overflow-y-scroll overflow-x-hidden snap-none">
{messageList.map((messageContent) => {
return (
<div
className={
username === messageContent.sender
? "h-auto padding-3 flex mt-2 justify-start"
: "h-auto padding-3 flex mt-2 justify-end"
}
>
<div>
<div
className={
username === messageContent.sender
? "h-auto w-auto h-min-40px w-max-120px bg-orange-300 rounded text-white flex align-center mx-1 p-1 break-words font-semibold justify-start"
: "h-auto w-auto h-min-40px w-max-120px bg-blue-600 rounded text-white flex align-center mx-1 p-1 break-words font-semibold justify-end"
}
>
<p>{messageContent.message}</p>
</div>
<div
className={
username === messageContent.sender
? "h-auto padding-3 flex mt-2 justify-start ml-3"
: "h-auto padding-3 flex mt-2 justify-end mr-5"
}
>
<p>{messageContent.time}</p>
<p className="ml-2 text-bold">{messageContent.sender}</p>
</div>
</div>
</div>
);
})}
</ScrollToBottom>
</div>
<div className="h-10 border border-black flex border-t-0">
<input
type="text"
value={currentMessage}
placeholder="Type a message"
onChange={(event) => {
setCurrentMessage(event.target.value);
}}
onKeyPress={(event) => {
event.key === "Enter" && sendMessage();
}}
className="h-full flex basis-4/5 outline-none px-3"
/>
<button
onClick={sendMessage}
className="h-full flex basis-1/5 justify-center outline-none bg-indigo-600 text-white font-bold text-xl"
>
send
</button>
</div>
</div>
);
}
export default ChatWidget;
start the app using the command npm start, the development server will start on the local address http://localhost:3000
Fixing the issue "Useffect code rendering twice":
The React Strict mode enabled in index.js will cause the issue of loading the useffect code to be rendered twice. To fix the issue update the code as mentioned below
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<App />
</>
);
Server - side Setup:
create new nest js app using the following command
nest new server && cd server
install the following packages
npm i @nestjs/platform-socket.io @nestjs/websockets @types/socket.io
update the src/main.ts file to add the CORS policy and listening port of the nestjs server.
export class SocketAdapter extends IoAdapter {
createIOServer(
port: number,
options?: ServerOptions & {
namespace?: string;
server?: any;
},
) {
const server = super.createIOServer(port, {
...options,
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
return server;
}
}
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, '..', 'static'));
app.enableCors({
origin: 'http://localhost:3000',
});
app.useWebSocketAdapter(new SocketAdapter(app));
await app.listen(3001);
}
bootstrap();
create new gateway for the chat module using the below command in the nest cli.
nest g gateway chat
This command will create a new file src/chat.gateway.ts. Add the below command to enable the socket.io to listen and emit to the client events.
import {
SubscribeMessage,
WebSocketGateway,
OnGatewayInit,
WebSocketServer,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { Logger } from '@nestjs/common';
@WebSocketGateway({
namespace: '/chat',
})
export class ChatGateway implements OnGatewayInit {
@WebSocketServer() wss: Server;
private logger: Logger = new Logger('ChatGateway');
afterInit(server: any) {
this.logger.log('Initialized!');
}
@SubscribeMessage('joinRoom')
handleRoomJoin(client: Socket, room: string) {
client.join(room);
client.emit('joinedRoom', room);
}
@SubscribeMessage('chatToServer')
handleMessage(
client: Socket,
message: { sender: string; room: string; message: string },
) {
this.wss.to(message.room).emit('chatToClient', message);
}
}
This above command will create a socket.io namespace in the url http://localhost:3001/chat.
you can get the entire source code from this github repository