Topic 9: Socket.io, The Front-end, and A Chat App
📖 5 min read · 🎯 advanced · 🧭 Prerequisites: hooks, navigation
Why this matters
Picture this: you open a WhatsApp group on your phone and a message appears — instantly, without you refreshing anything. That's not magic, that's a persistent connection. Up until now, every feature we've built follows a simple pattern: you ask, the server answers, connection closes. But real-time chat doesn't work that way. You need the server to push messages to you the moment someone else sends one. That's exactly what Socket.io does — it keeps a live connection open between your React frontend and your Node.js backend, so every message travels instantly to every connected screen.
What You'll Learn
- Scaffold a Node.js + Express server and attach Socket.io to it
- Listen for and broadcast
chat messageevents across all connected clients - Build a React
Chatcomponent that connects to the Socket.io server, sends messages, and updates state in real time - Wire the
Chatcomponent into anApproot and run the full stack locally
The Analogy
Think of Socket.io like a walkie-talkie dispatch network. A traditional HTTP request is a letter: you send it, wait for a reply, and the connection closes. Socket.io is the open channel that stays live — when anyone keys up and speaks, the dispatcher (the server) instantly rebroadcasts to every radio on the network. You don't poll the channel asking "did anyone talk?"; the transmission arrives the moment it happens. Building a chat app with Socket.io is exactly that: open a persistent channel per client, let any client broadcast a message event, and have the server relay it to every listener simultaneously.
Chapter 1: Setting Up the Backend with Socket.io
Socket.io enables real-time, bidirectional communication between web clients and servers over a persistent connection — falling back gracefully from WebSockets to long-polling when needed.
Step 1: Create a New Node.js Project
mkdir chat-app
cd chat-app
npm init -y
Step 2: Install Dependencies
npm install express socket.io
Step 3: Set Up the Server
Create server.js. The key pattern is:
- Create an
http.Serverwrapping the Express app (Socket.io needs the raw HTTP server, not just Express). - Pass that server to
socketIo()to get theioinstance. - Listen for the
connectionevent to receive individualsockethandles. - On each socket, listen for
chat messageevents and re-emit them to all clients withio.emit.
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
io.on('connection', (socket) => {
console.log('New client connected');
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
const PORT = process.env.PORT || 4000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key points:
io.emitbroadcasts to every connected socket, including the sender.socket.emitwould send only back to the originating client.socket.broadcast.emitsends to everyone except the sender.- The
disconnectevent fires automatically when a client closes the tab or loses connectivity.
sequenceDiagram
participant ClientA
participant Server
participant ClientB
ClientA->>Server: connect
ClientB->>Server: connect
ClientA->>Server: emit("chat message", "Hello!")
Server->>ClientA: emit("chat message", "Hello!")
Server->>ClientB: emit("chat message", "Hello!")
Chapter 2: Setting Up the Frontend with React
Step 1: Create a New React Project
npx create-react-app chat-app-client
cd chat-app-client
Step 2: Install the Socket.io Client Library
npm install socket.io-client
Step 3: Create the Chat Component
Create src/components/Chat.js. The socket connection is opened once at module scope — outside the component function — so it is not re-created on every render.
import React, { useState, useEffect } from 'react';
import io from 'socket.io-client';
const socket = io('http://localhost:4000');
function Chat() {
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
useEffect(() => {
socket.on('chat message', (msg) => {
setMessages((prevMessages) => [...prevMessages, msg]);
});
return () => {
socket.off('chat message');
};
}, []);
const sendMessage = (e) => {
e.preventDefault();
socket.emit('chat message', message);
setMessage('');
};
return (
<div>
<ul>
{messages.map((msg, index) => (
<li key={index}>{msg}</li>
))}
</ul>
<form onSubmit={sendMessage}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
export default Chat;
Three things to notice:
useEffectcleanup —socket.off('chat message')removes the listener when the component unmounts, preventing duplicate handlers if the component ever re-mounts.- Functional state update —
setMessages((prevMessages) => [...prevMessages, msg])avoids stale-closure bugs by always spreading the latest array. socket.emitsends the typed message to the server; the server then re-broadcasts it back to everyone, including this client, which is what appends it to the local list.
Step 4: Integrate the Chat Component into App
import React from 'react';
import './App.css';
import Chat from './components/Chat';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>Real-Time Chat App</h1>
</header>
<Chat />
</div>
);
}
export default App;
Step 5: Start the React App
npm start
The React development server starts at http://localhost:3000. Make sure the Node server is already running on port 4000 before loading the frontend.
Chapter 3: Testing the Chat Application
With both servers running, open two browser tabs to http://localhost:3000. Type a message in one tab and hit Send. The message appears instantly in both tabs — that is Socket.io's bidirectional broadcast in action. Open a third tab; it will receive every new message too. Close a tab and watch the server log Client disconnected.
Testing checklist:
- Messages sent from Tab A appear in Tab B without any page refresh
- Multiple tabs all receive the same broadcast
- Closing a tab logs the disconnect event on the server
- Refreshing a tab re-establishes the connection and the message log restarts from empty (the server holds no message history in this basic implementation)
🧪 Try It Yourself
Task: Add a username to each message so recipients know who is speaking.
- On the frontend, add a second piece of state:
const [username, setUsername] = useState(''); - Add a name input field above the chat form that sets
username. - Change
socket.emit('chat message', message)to emit an object instead:
socket.emit('chat message', { user: username, text: message });
- Update the server to re-broadcast the object as-is:
socket.on('chat message', (msgObj) => {
io.emit('chat message', msgObj);
});
- Update the frontend list to render
{msg.user}: {msg.text}instead of justmsg.
Success criterion: Open two tabs, give each a different name, and send messages. Each message in both tabs should display as Alice: Hello! or Bob: Hey there!.
🔍 Checkpoint Quiz
Q1. Why does Socket.io use http.createServer(app) and pass that to socketIo() instead of just passing the Express app directly?
Q2. Given this server snippet:
socket.on('chat message', (msg) => {
socket.emit('chat message', msg);
});
What is the difference in behavior compared to using io.emit('chat message', msg)?
A) socket.emit broadcasts to all connected clients; io.emit sends only to the sender
B) socket.emit sends only back to the originating client; io.emit broadcasts to all clients
C) There is no difference — both emit to all clients
D) socket.emit broadcasts to all clients except the sender; io.emit sends to everyone including the sender
Q3. In the Chat component, why is the socket.off('chat message') call placed inside the useEffect cleanup return?
A) To prevent the socket from connecting until the component mounts
B) To remove the event listener when the component unmounts, avoiding duplicate handlers on remount
C) To disconnect the socket entirely when the user navigates away
D) socket.off is required by the Socket.io client API on every render
Q4. How would you modify the current app so that a new client joining the chat immediately sees the last 10 messages sent before they connected?
A1. Socket.io needs direct access to the raw Node.js http.Server instance to intercept the WebSocket upgrade handshake. Express itself is just a request handler; it does not expose the upgrade mechanism that persistent WebSocket connections require. Wrapping Express in http.createServer gives Socket.io the low-level server handle it needs.
A2. B — socket.emit targets only the socket that triggered the event (the sender), while io.emit broadcasts to every currently connected socket including the sender. The current server correctly uses io.emit so all participants see every message.
A3. B — useEffect runs after every mount. If the component unmounts and remounts (e.g., during React Strict Mode double-invocation or route changes), without the cleanup the same listener would be registered twice, causing every incoming message to be appended twice to the messages array.
A4. On the server, maintain an in-memory array (e.g., const history = []) and push each incoming message to it (capping at 10 entries). In the connection handler, after the socket connects, emit the history to only that socket: socket.emit('message history', history). On the client, add a second socket.on('message history', (msgs) => setMessages(msgs)) listener inside useEffect to seed the initial state with the history array.
🪞 Recap
- Socket.io wraps a Node.js
http.Serverto enable persistent, bidirectional WebSocket connections with automatic fallback. io.emitbroadcasts an event to every connected client;socket.emittargets only the originating connection.- The React
socket.io-clientis instantiated once at module scope and shared across renders to avoid creating multiple connections. useEffectcleanup withsocket.offprevents listener accumulation when components remount.- The full stack runs with the Node server on port 4000 and the React dev server on port 3000, with the client explicitly connecting to
http://localhost:4000.
📚 Further Reading
- Socket.io Official Documentation — the source of truth on namespaces, rooms, acknowledgements, and adapters
- Socket.io Client API Reference — full list of client-side methods including
on,off,emit, and connection options - [Using WebSockets with React — PLACEHOLDER] — deeper dive into managing socket lifecycles inside React's concurrency model
- Node.js
httpmodule docs — explains whyhttp.createServeris needed as the Socket.io attachment point - ⬅️ Previous: Navigation
- ➡️ Next: Using Other Open Source Material