Topic 24 of 56 · Full Stack Advanced

Topic 9 : Socket.io, The Front-end, and A Chat App

Lesson TL;DRTopic 9: Socket.io, The Frontend, 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 ...
5 min read·advanced·socket-io · real-time · websockets · react

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 message events across all connected clients
  • Build a React Chat component that connects to the Socket.io server, sends messages, and updates state in real time
  • Wire the Chat component into an App root 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:

  1. Create an http.Server wrapping the Express app (Socket.io needs the raw HTTP server, not just Express).
  2. Pass that server to socketIo() to get the io instance.
  3. Listen for the connection event to receive individual socket handles.
  4. On each socket, listen for chat message events and re-emit them to all clients with io.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.emit broadcasts to every connected socket, including the sender.
  • socket.emit would send only back to the originating client.
  • socket.broadcast.emit sends to everyone except the sender.
  • The disconnect event 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:

  • useEffect cleanupsocket.off('chat message') removes the listener when the component unmounts, preventing duplicate handlers if the component ever re-mounts.
  • Functional state updatesetMessages((prevMessages) => [...prevMessages, msg]) avoids stale-closure bugs by always spreading the latest array.
  • socket.emit sends 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.

  1. On the frontend, add a second piece of state: const [username, setUsername] = useState('');
  2. Add a name input field above the chat form that sets username.
  3. Change socket.emit('chat message', message) to emit an object instead:
socket.emit('chat message', { user: username, text: message });
  1. Update the server to re-broadcast the object as-is:
socket.on('chat message', (msgObj) => {
    io.emit('chat message', msgObj);
});
  1. Update the frontend list to render {msg.user}: {msg.text} instead of just msg.

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.Server to enable persistent, bidirectional WebSocket connections with automatic fallback.
  • io.emit broadcasts an event to every connected client; socket.emit targets only the originating connection.
  • The React socket.io-client is instantiated once at module scope and shared across renders to avoid creating multiple connections.
  • useEffect cleanup with socket.off prevents 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

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.