MERN

Building Real-Time Apps with MERN Stack & Socket.io

dev.prakah2011 March 30, 2025 2 min read
🍃

Real-time features — live notifications, collaborative editing, presence indicators, and chat — are no longer optional for modern web applications. Socket.io sits on top of WebSockets and provides an elegant abstraction that works across every browser, falling back gracefully where native WebSockets are unavailable.

Project Structure

mern-realtime/
  server/          ← Express + Socket.io
    index.js
    events/
      chat.js
      notifications.js
  client/          ← React + Socket.io-client
    src/
      hooks/
        useSocket.js
      components/
        ChatRoom.jsx
        NotificationBell.jsx

1. Setting Up the Socket.io Server

// server/index.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: process.env.CLIENT_URL,
    methods: ['GET', 'POST'],
  },
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.user = verifyJWT(token);
    next();
  } catch (e) {
    next(new Error('Authentication error'));
  }
});

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.user.id}`);

  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', { userId: socket.user.id });
  });

  socket.on('send-message', ({ roomId, text }) => {
    const message = { id: uuid(), userId: socket.user.id, text, timestamp: Date.now() };
    io.to(roomId).emit('new-message', message);
    // Persist to MongoDB
    Message.create({ ...message, room: roomId });
  });

  socket.on('disconnect', () => {
    console.log(`User disconnected: ${socket.user.id}`);
  });
});

httpServer.listen(4000);

2. Reusable React Hook

// client/src/hooks/useSocket.js
import { useEffect, useRef } from 'react';
import { io } from 'socket.io-client';

export function useSocket(token) {
  const socketRef = useRef(null);

  useEffect(() => {
    socketRef.current = io(process.env.REACT_APP_API_URL, {
      auth: { token },
    });

    return () => {
      socketRef.current?.disconnect();
    };
  }, [token]);

  return socketRef.current;
}

3. Presence Indicators

Track who is online by maintaining a Map of connected user IDs on the server:

const onlineUsers = new Map(); // userId → socketId

io.on('connection', (socket) => {
  onlineUsers.set(socket.user.id, socket.id);
  io.emit('online-users', [...onlineUsers.keys()]);

  socket.on('disconnect', () => {
    onlineUsers.delete(socket.user.id);
    io.emit('online-users', [...onlineUsers.keys()]);
  });
});

4. Scaling with Redis Adapter

Once you add a second server instance, in-memory state no longer works. Switch to the Socket.io Redis Adapter so events are broadcast across all instances:

const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

With this setup your application scales horizontally behind a load balancer with no code changes to your event handlers.

dev.prakah2011
dev.prakah2011

Developer & author at DevForge Agency.

Related Articles

🍃
MERN

Deploying a MERN App to AWS with Docker & GitHub Actions