Topic 9 of 18 · Node Expert

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: addingmiddleware, buildingahttpserverwithnodejsusinghttpapis Why this matters Up until now, every time ...
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: adding-middleware, building-a-http-server-with-node-js-using-http-apis

Why this matters

Up until now, every time your front-end needed fresh data, it had to ask — send a request, wait for a response, then ask again. That works fine for loading a page. But imagine building a chat app that way: you type a message, hit send, and the other person only sees it when they happen to refresh. That's not a chat app, that's email with extra steps.

Socket.io changes this completely. It keeps a live, two-way connection open between the browser and your Node server — so the moment someone sends a message, every connected tab gets it instantly. By the end of this lesson, you'll have a working real-time chat app running in the browser, powered by Socket.io.

What You'll Learn

  • How Socket.io enables real-time, bidirectional communication between clients and a Node.js server
  • How to bootstrap a Node.js/Express backend integrated with Socket.io
  • How to build a React frontend Chat component that connects to the socket server
  • How to wire the full stack together so messages flow live across multiple browser tabs

The Analogy

Think of a traditional HTTP chat like passing written notes through a postal service — you write a note, hand it to the courier, they carry it to the recipient, who hands a reply back through the same chain. Every message is a full round trip. Socket.io is more like a telephone party line: once everyone picks up and joins the call, anyone can speak and every other caller hears the words in real time without hanging up and redialing. The connection stays open, the channel stays warm, and the overhead of re-establishing the call disappears entirely.

Chapter 1: Setting Up the Backend with Socket.io

Socket.io gives you a persistent, event-driven channel layered on top of WebSockets (with graceful fallbacks for environments that don't support them). The server side works by emitting and listening to named events; the client mirrors the same API.

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

express handles HTTP routing and serves static assets. socket.io owns the real-time layer and requires a raw http.Server instance — not the Express app directly.

Step 3 — Set Up the Server

server.js:

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 in this file:

  • http.createServer(app) wraps the Express app in a raw Node.js HTTP server so Socket.io can attach its WebSocket upgrade handler to the same port.
  • io.on('connection', callback) fires once per new client connection and hands you that client's individual socket object.
  • Inside the connection callback, socket.on('chat message', ...) listens for a named event from that one client.
  • io.emit('chat message', msg) broadcasts the message to every connected client — including the sender. Use socket.broadcast.emit if you want to exclude the sender.
  • socket.on('disconnect', ...) cleans up when a tab closes or the network drops.
sequenceDiagram
    participant BrowserA
    participant BrowserB
    participant Server

    BrowserA->>Server: connect (WebSocket upgrade)
    BrowserB->>Server: connect (WebSocket upgrade)
    BrowserA->>Server: emit('chat message', 'Hello!')
    Server->>BrowserA: emit('chat message', 'Hello!')
    Server->>BrowserB: 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 Socket.io Client

npm install socket.io-client

socket.io-client is the browser counterpart to the server library. It speaks the same event protocol and handles reconnection logic automatically.

Step 3 — Create the Chat Component

src/components/Chat.js:

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;

Important design decisions here:

  • socket is created outside the component so a single persistent connection is shared across re-renders rather than creating a new socket on every render cycle.
  • The useEffect with an empty dependency array [] registers the 'chat message' listener once on mount.
  • The cleanup function socket.off('chat message') removes the listener on unmount to prevent duplicate handlers if the component is ever remounted (common in Strict Mode).
  • setMessages((prevMessages) => [...prevMessages, msg]) uses the functional updater form of setState so incoming socket events always close over the latest state, not a stale snapshot.

Step 4 — Integrate Chat Component into the App

src/App.js:

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. The backend is running on port 4000, so the two processes run side by side without conflict.

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 click Send — the message appears instantly in both tabs without a page refresh. This proves the bidirectional socket connection is alive and that io.emit on the server fans the event out to every connected client.

Checklist for a successful test:

  • Server console logs New client connected twice (once per tab).
  • Message typed in Tab A appears in Tab A and Tab B simultaneously.
  • Closing a tab logs Client disconnected in the server console.
  • Opening a third tab and typing shows the message in all three tabs.

🧪 Try It Yourself

Task: Add a username to each chat message so recipients know who sent it.

  1. In the React Chat component, add a username state field and a text input for it (rendered above the message list, only shown until the user picks a name).
  2. Change socket.emit('chat message', message) to emit an object instead:
socket.emit('chat message', { user: username, text: message });
  1. On the server, relay the full object:
socket.on('chat message', (msg) => {
    io.emit('chat message', msg);
});
  1. In the <ul> renderer, display msg.user: msg.text instead of msg.

Success criterion: Sending a message from a tab where you entered the name "Alice" should render as Alice: Hello! in every open tab.

🔍 Checkpoint Quiz

Q1. Why does socket.io require http.createServer(app) rather than just using the Express app directly?

A) Express doesn't support listen() B) Socket.io needs to attach a WebSocket upgrade handler to the raw HTTP server, which Express doesn't expose C) http.createServer adds CORS headers automatically D) It's just a convention — either works fine

Q2. Given the server code below, what happens when Client A sends a 'chat message' event?

io.on('connection', (socket) => {
    socket.on('chat message', (msg) => {
        io.emit('chat message', msg);
    });
});

A) Only Client A receives the message back B) Every connected client except Client A receives the message C) Every connected client including Client A receives the message D) The server crashes because io.emit is called inside a socket handler

Q3. In the React Chat component, why is the socket instance declared outside the component function rather than inside a useEffect?

A) React hooks can't be used alongside socket.io B) Declaring it outside creates a single persistent connection shared across all renders; declaring it inside would create a new socket on every re-render C) useEffect doesn't run on the first render D) socket.io-client requires a global variable

Q4. You want to broadcast a message to everyone except the sender. Which server-side call should you use instead of io.emit?

A) socket.emit('chat message', msg) B) io.broadcast('chat message', msg) C) socket.broadcast.emit('chat message', msg) D) io.to('others').emit('chat message', msg)

A1. B — Socket.io performs a WebSocket upgrade handshake at the HTTP level. It attaches an upgrade event listener to the raw http.Server; the Express app object has no such mechanism.

A2. C — io.emit sends the event to every socket currently connected to the server, including the originating socket.

A3. B — Module-level variables in JavaScript are initialized once when the file is first imported. Placing socket outside the component ensures one WebSocket connection per browser tab, not one per render cycle. Inside a useEffect with [] would also work but is less common because it requires a ref to avoid stale closures.

A4. C — socket.broadcast.emit sends to all connected sockets except the one calling it, which is the standard pattern for "tell everyone else what I just said."

🪞 Recap

  • Socket.io wraps WebSockets with a named-event API and automatic fallbacks, giving you persistent bidirectional communication without polling.
  • The server attaches to a raw http.Server instance; io.on('connection') fires per client and hands you that client's socket for targeted or broadcast messaging.
  • The React client imports socket.io-client, creates one shared socket outside the component, and uses useEffect cleanup (socket.off) to avoid duplicate listeners on remount.
  • io.emit fans an event to every connected client; socket.broadcast.emit excludes the sender; socket.emit sends only back to the originating client.
  • Testing with multiple tabs is the fastest way to confirm real-time fan-out is working correctly.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

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