Time to put everything from Phase 3 together into one complete, working feature: a real chat application. This post combines connection handling, typed events, and both directions of data flow into a single page.
Step 1: Update Our Shared Types
Open src/types/socket.ts and update it with everything our chat app needs:
export interface ChatMessage { id: string; text: string; sender: string; timestamp: number; }
export interface ServerToClientEvents { message: (data: { text: string; sender: string }) => void; hello: (text: string) => void; newMessage: (data: ChatMessage) => void; onlineCount: (count: number) => void; }
export interface ClientToServerEvents { sendMessage: (data: { text: string; sender: string }) => void; sayHello: () => void; chatMessage: (data: { text: string; sender: string }) => void; }
export interface InterServerEvents { ping: () => void; }
export interface SocketData { userId: string; }
Step 2: Update the Server
Open server.ts and update the connection handler to track online users and broadcast chat messages:
import { createServer } from "http"; import { parse } from "url"; import next from "next"; import { Server } from "socket.io"; import type { ServerToClientEvents, ClientToServerEvents, InterServerEvents, SocketData, } from "./src/types/socket";
const dev = process.env.NODE_ENV !== "production"; const hostname = "localhost"; const port = parseInt(process.env.PORT || "3000", 10);
const app = next({ dev, hostname, port }); const handle = app.getRequestHandler();
app.prepare().then(() => { const httpServer = createServer((req, res) => { const parsedUrl = parse(req.url!, true); handle(req, res, parsedUrl); });
const io = new Server< ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData >(httpServer, { path: "/api/socket", });
let onlineCount = 0;
io.on("connection", (socket) => { console.log("A client connected:", socket.id);
onlineCount++; io.emit("onlineCount", onlineCount);
socket.on("chatMessage", (data) => { io.emit("newMessage", { id: `${socket.id}-${Date.now()}`, text: data.text, sender: data.sender, timestamp: Date.now(), }); });
socket.on("sayHello", () => { socket.emit("hello", "Hello from the server!"); });
socket.on("sendMessage", (data) => { console.log("Received:", data.text, "from", data.sender); });
socket.on("disconnect", () => { onlineCount--; io.emit("onlineCount", onlineCount); console.log("Client disconnected:", socket.id); }); });
httpServer.listen(port, () => { console.log(`Server ready on http://${hostname}:${port}`); }); });
Notice we use io.emit("newMessage", ...) here, not socket.broadcast.emit(...). This is intentional for a simple chat UI: instead of managing local state separately for "my own message" versus "others' messages," we let the server be the single source of truth, and every client, including the sender, receives the message the same way, through this one event.
Step 3: Build the Chat UI
Replace src/app/page.tsx with this complete chat interface:
"use client";
import { useEffect, useRef, useState } from "react"; import { socket } from "@/lib/socket"; import type { ChatMessage } from "@/types/socket";
export default function Home() { const [isConnected, setIsConnected] = useState(false); const [onlineCount, setOnlineCount] = useState(0); const [messages, setMessages] = useState<ChatMessage[]>([]); const [input, setInput] = useState(""); const [username] = useState( () => `User-${Math.floor(Math.random() * 1000)}` );
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => { setIsConnected(socket.connected);
function onConnect() { setIsConnected(true); }
function onDisconnect() { setIsConnected(false); }
function onNewMessage(data: ChatMessage) { setMessages((prev) => [...prev, data]); }
function onOnlineCount(count: number) { setOnlineCount(count); }
socket.on("connect", onConnect); socket.on("disconnect", onDisconnect); socket.on("newMessage", onNewMessage); socket.on("onlineCount", onOnlineCount);
return () => { socket.off("connect", onConnect); socket.off("disconnect", onDisconnect); socket.off("newMessage", onNewMessage); socket.off("onlineCount", onOnlineCount); }; }, []);
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);
function handleSend() { if (input.trim() === "") return; socket.emit("chatMessage", { text: input, sender: username }); setInput(""); }
return ( <main className="flex min-h-screen flex-col items-center bg-gray-900 text-white p-4"> <div className="flex w-full max-w-md items-center justify-between mb-4"> <span className={`text-sm ${isConnected ? "text-green-400" : "text-red-400" }`} > {isConnected ? "Connected" : "Disconnected"} </span> <span className="text-sm text-gray-400"> {onlineCount} online </span> </div>
<div className="w-full max-w-md flex-1 overflow-y-auto rounded-lg bg-gray-800 p-4 h-96"> {messages.map((msg) => ( <div key={msg.id} className={`mb-2 flex flex-col ${msg.sender === username ? "items-end" : "items-start" }`} > <span className="text-xs text-gray-400">{msg.sender}</span> <span className={`rounded-lg px-3 py-2 text-sm ${msg.sender === username ? "bg-blue-600" : "bg-gray-700" }`} > {msg.text} </span> </div> ))} <div ref={bottomRef} /> </div>
<div className="mt-4 flex w-full max-w-md gap-2"> <input value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSend()} placeholder="Type a message..." className="flex-1 rounded bg-gray-800 px-3 py-2 text-sm outline-none" /> <button onClick={handleSend} className="rounded bg-blue-600 px-4 py-2 text-sm hover:bg-blue-700" > Send </button> </div> </main> ); }
Step 4: Understanding the Key Parts
usernameis generated once usinguseState(() => ...), so it stays the same for the life of the component instead of regenerating on every re-renderbottomRefcombined with the seconduseEffectgives us auto-scroll: every timemessageschanges, we scroll a hidden div at the bottom into view smoothly- Messages are styled differently depending on whether
msg.sender === username, giving the classic chat bubble look, your own messages on the right, others on the left onKeyDownlets users press Enter to send, in addition to clicking the button- The
onlineCountbadge updates live as users connect and disconnect, using the events we set up on the server
Step 5: Test With Multiple Tabs
Run npm run dev, then open http://localhost:3000 in two or three separate browser tabs. Send a message from one tab, and you should see it appear instantly in every other tab. Close a tab, and watch the online count decrease in the remaining ones.
A Known Limitation
Right now, messages only exist in memory, inside the messages state on each client, and briefly inside your server's event flow. If you refresh the page, chat history disappears completely, since nothing is being saved anywhere. This is expected at this stage. Adding persistent storage, like a database, is outside the scope of this Socket.IO course, but worth keeping in mind if you extend this project further.
Summary
- We combined typed events, connection tracking, and bidirectional data flow into one working chat feature
- The server broadcasts every message to all clients using
io.emit, keeping the server as the single source of truth - Auto-scroll was added using
useRefand auseEffectthat runs whenevermessageschanges - Online user count updates live, using the connect/disconnect tracking pattern from the previous post
- Chat history is not persisted, it only exists in memory for the current session
In the next post, we go into Rooms, learning how to group specific users together so messages can be sent only to a particular group instead of everyone.
No comments:
Post a Comment