I recently built a real time chat application using sockets. You can check it out at chatify-rtc.vercel.app and view source code at github.com/VaibhavArora314/real-time-chat. Make sure to check out the project and if u like don’t forget to star the repo.
Tech Stack:
- TypeScript
- Express.js
- React.js
- Recoil
- Socket.io
- MongoDb
- Tailwind
In this article I will explain the sockets part of my website:
Lets start with the socket setup on the backend side. From the documentation read how to install and setup sockets.
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import cors from "cors";
import rootRouter from "./routes";
import bodyParser from "body-parser";
import { decodeJWT, verifyJWT } from "./helpers/jwt";
import { config } from "dotenv";
config();
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use("/api/v1", rootRouter);
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: process.env.FRONTEND_URL || "http://localhost:5173",
},
});
io.on("connection", async (socket) => {
console.log(
`User connected using socket id ${socket.id}!`
);
socket.on("disconnect", () => {
console.log(`User disconnected ${socket.id}`);
});
});
httpServer.listen(3000);
Now add a socket middleware to only allow existing users to connect. Also here CONNECTED_USERS
is an in memory array of user ids to same users to connect multiple time simultaneously. Also we need the user to join all the rooms he is present in as soon as he connects.
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token || !verifyJWT(token)) {
return next(new Error("Invalid token!"));
}
const decodedValue: any = decodeJWT(token);
const userId = decodedValue.userId;
if (CONNECTED_USERS.includes(userId)) {
return next(new Error("One connection already exists for this user!"));
}
socket.data.userId = decodedValue.userId;
next();
}).on("connection", async (socket) => {
const userId = socket.data.userId;
CONNECTED_USERS.push(userId);
const user = await User.findOne({ _id: userId });
user?.rooms.forEach((roomId) => {
socket.join(roomId.toString());
});
console.log(
`User ${user?.username} connected using socket id ${socket.id} and present in ${socket.rooms.size} rooms initially!`
);
socket.on("disconnect", () => {
const index = CONNECTED_USERS.indexOf(userId);
if (index > -1) CONNECTED_USERS.splice(index, 1);
console.log(`User disconnected ${socket.id}`);
});
});
Now lets add socket events. Create 4 files as follows:
send_message.ts:
const sendMessageHandler = async (
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
userId: string | null,
roomId: string,
message: string
) => {
try {
// verify and create message here
io.to(roomId).emit("receive_message", returnvalue);
} catch (error) {
console.log(
`Error occurred while sending message in room ${roomId} by user ${userId}!`
);
// console.log(error?.message);
}
};
export default sendMessageHandler;
create_room.ts
const createRoomSchema = zod.object({
title: zod.string().min(5).max(40),
description: zod.string().max(100),
})
const createRoomHandler = async (
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
socket: Socket,
userId: string,
title: string,
description: string,
) => {
try {
const result = createRoomSchema.safeParse({title,description});
if (!result.success) return;
// create room logic here
io.to(socket.id).emit("joined_room", {room: formatRoom(room,userId), message: `Successfully created room ${title}`});
socket.join(room._id.toString());
await sendMessageHandler(io,null,room._id.toString(),`${user?.username} created the room`);
} catch (error) {
console.log("Error occurred!", error);
}
};
export default createRoomHandler;
join_room.ts:
const joinRoomHandler = async (
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
socket: Socket,
userId: string,
inviteCode: string
) => {
try {
// verify invite code and join room
io.to(socket.id).emit("joined_room", {
room: formatRoom(populatedRoom, userId),
message: `Successfully joined room ${room.title}`,
});
socket.join(room._id.toString());
await sendMessageHandler(
io,
null,
room._id.toString(),
`${user?.username} joined the room`
);
} catch (error) {
console.log("Error occurred!", error);
}
};
export default joinRoomHandler;
leave_room.ts:
const leaveRoomHandler = async (
io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>,
socket: Socket,
userId: string,
roomId: string
) => {
// update room and remove this user from its participants and also update user's rooms here
io.to(socket.id).emit("left_room", { roomId });
socket.leave(roomId);
if (room && room?.participants.length > 0)
await sendMessageHandler(
io,
null,
roomId,
`${user?.username} left the room`
);
else {
//delete the room and its messages since no one present now
}
};
export default leaveRoomHandler;
Now using these functions, we will add socket events:
socket.on(
"send_message",
async ({ roomId, message }: { roomId: string; message: string }) => {
await sendMessageHandler(io, userId, roomId, message);
}
);
socket.on(
"create_room",
async ({ title, description }: { title: string; description: string }) => {
await createRoomHandler(io, socket, userId, title, description);
}
);
socket.on("join_room", async ({ inviteCode }: { inviteCode: string }) => {
await joinRoomHandler(io, socket, userId, inviteCode);
});
socket.on("leave_room", async ({ roomId }: { roomId: string }) => {
await leaveRoomHandler(io, socket, userId, roomId);
});
This is what final code will look like:
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token || !verifyJWT(token)) {
return next(new Error("Invalid token!"));
}
const decodedValue: any = decodeJWT(token);
const userId = decodedValue.userId;
if (CONNECTED_USERS.includes(userId)) {
return next(new Error("One connection already exists for this user!"));
}
socket.data.userId = decodedValue.userId;
next();
}).on("connection", async (socket) => {
const userId = socket.data.userId;
CONNECTED_USERS.push(userId);
const user = await User.findOne({ _id: userId });
user?.rooms.forEach((roomId) => {
socket.join(roomId.toString());
});
console.log(
`User ${user?.username} connected using socket id ${socket.id} and present in ${socket.rooms.size} rooms initially!`
);
socket.on(
"send_message",
async ({ roomId, message }: { roomId: string; message: string }) => {
await sendMessageHandler(io, userId, roomId, message);
}
);
socket.on(
"create_room",
async ({ title, description }: { title: string; description: string }) => {
await createRoomHandler(io, socket, userId, title, description);
}
);
socket.on("join_room", async ({ inviteCode }: { inviteCode: string }) => {
await joinRoomHandler(io, socket, userId, inviteCode);
});
socket.on("leave_room", async ({ roomId }: { roomId: string }) => {
await leaveRoomHandler(io, socket, userId, roomId);
});
socket.on("disconnect", () => {
const index = CONNECTED_USERS.indexOf(userId);
if (index > -1) CONNECTED_USERS.splice(index, 1);
console.log(`User disconnected ${socket.id}`);
});
});
Now lets setup a component in the react application for handling the socket events:
// Imports here
const DashboardWrapper = () => {
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [socket, setSocket] = useState<Socket | null>(null);
const handleReceiveMessage = useRecoilCallback(
({ set }) =>
(message: MessageInteface, roomName: string) => {
// add message to recoil state here
},
[]
);
const handleRoomJoin = useRecoilCallback(
({ set }) =>
(room: RoomInfoInteface, message: string) => {
// add joined room to recoil state
},
[]
);
const handleLeaveRoom = useRecoilCallback(
({ set }) =>
(roomId: string) => {
// remove room from recoil state
},
[]
);
useEffect(() => {
const newSocket = io(BACKEND_URL, {
auth: {
token,
}
});
newSocket.on("connect", () => {
if (loading) setLoading(false);
});
newSocket.on("connect_error", (err) => {
setError(err.message);
setLoading(false);
});
newSocket.on(
"joined_room",
(data: { room: RoomInfoInteface; message: string }) => {
handleRoomJoin(data.room, data.message);
}
);
newSocket.on("receive_message", (data: { message: MessageInteface, roomName:string }) => {
handleReceiveMessage(data.message,data.roomName);
});
newSocket.on("left_room", (data: { roomId: string }) => {
handleLeaveRoom(data.roomId);
});
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}, [token]);
if (loading) return <Loader />;
if (error) return <RedirectMessageComponent message={error} />;
return (
<>
<Navbar />
<Dashboard socket={socket} />
</>
);
};
export default DashboardWrapper;
const Dashboard = ({ socket }) => {
// state variables
const sendMessage = (message: string, roomId: string) => {
if (!message || message.length == 0) return "";
socket?.emit("send_message", { message, roomId });
};
const joinRoom = (inviteCode: string) => {
socket?.emit("join_room", {
inviteCode,
});
};
const createRoom = (title: string, description: string) => {
socket?.emit("create_room", {
title,
description,
});
};
const leaveRoom = (roomId: string) => {
socket?.emit("leave_room", {
roomId
})
}
return (
<>
{/* Return jsx here */}
</>
);
};