DEV Community

Cover image for Socket.IO for Beginners: Build a Live Online Users Tracker with MERN
Minhaj Tapader
Minhaj Tapader

Posted on

Socket.IO for Beginners: Build a Live Online Users Tracker with MERN

What is Socket.IO?

Socket.IO is a JavaScript library used for real-time data exchange between client and server.

Why should you use Socket.IO?

You can use Socket.IO when you need to implement real-time features in your project. With this, you can update the data in real time. Once connected, both the client and the server can exchange data in real time. Socket.IO uses Event based communication. For example, you can create an event and then send and receive it to both the client and the server. With the help of Socket.IO you can handle hundreds of connections if you want. Socket.IO can be used in various places such as: chat applications, online games, real-time notifications, etc.

Required Setup:

  1. You need to have a basic understanding of JavaScript, React.js, Node.js, Mongoose, and npm.
  2. VS Code and Node.js must be installed.

Project Setup:

Now I will create a folder named project. This will be the main project folder. Inside it, I will create two more folders named client and server.

Creating a project directory:

mkdir project
cd project
mkdir server
mkdir client
Enter fullscreen mode Exit fullscreen mode

Create Backend:

cd server
npm init -y
npm i mongoose express cors dotenv nodemon validator
npm i socket.io (This package is used to implement socket.IO)
code .

Enter fullscreen mode Exit fullscreen mode

Create Express server:

  • Inside server/index.js, write the server code to initialize the server.
import express from 'express';
import dotenv from 'dotenv';
import { app, server } from './socket/socket.js';
import cors from 'cors';

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
import connectDB from './config/db.js';
import userRoutes from './route/userRoute.js';

dotenv.config();
connectDB();
const PORT = process.env.PORT || 3000;

app.use('/api/users', userRoutes);
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode
  • Inside socket / socket.js Here you need to write the code to initialize the socket and then connect to the server. You can create any real time event within the connection. Here I am just creating an event to see how many people are online after creating the user and sending it to the UI. The getOnlineUsers event will return an array that contains the IDs of the online users.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: process.env.CLIENT_URL,
    methods: ['GET', 'POST'],
  },
});
let userConnections = {};
export const getReciverSocketId = (reciverId) => userConnections[reciverId];

io.on('connection', (socket) => {
  console.log('socket', socket.handshake.query);
  const { userId } = socket.handshake.query;
  if (userId) {
    userConnections[userId] = socket.id;
    console.log(`User ${userId} connected and socket id is ${socket.id}`);
  }
  io.emit('getOnlineUsers', Object.keys(userConnections));
  console.log('userConnections', userConnections);
  socket.on('disconnect', () => {
    if (userId) {
      delete userConnections[userId];
      console.log(`User ${userId} disconnected and socket id is ${socket.id}`);
    }
    io.emit('getOnlineUsers', Object.keys(userConnections));
  });
});
export { app, server, io };
Enter fullscreen mode Exit fullscreen mode
  • Inside model / userModel.js Here I am creating a mongoose model to register the user. To keep it simple, I have created a model with only username and email.
import mongoose from 'mongoose';
import validator from 'validator';

const userSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      required: [true, 'Email is required'],
      unique: true,
      validate: {
        validator: validator.isEmail,
        message: 'Please enter a valid email',
      },
    },

    username: {
      type: String,
      required: [true, 'Username is required'],
      minlength: [3, 'Username must be at least 3 characters'],
    },
  },
  {
    timestamps: true,
  }
);
const User = mongoose.model('User', userSchema);
export default User;
Enter fullscreen mode Exit fullscreen mode
  • Inside controller / userController.js This is where the user register and all user-related logics are written.
import User from '../model/userModel.js';

export const register = async (req, res) => {
  try {
    const { username, email } = req.body;
    if (!username || !email) {
      return res
        .status(401)
        .json({ message: 'All fields are required', success: false });
    }
    const userExistsEmail = await User.findOne({ email });

    if (userExistsEmail) {
      return res
        .status(401)
        .json({ message: 'User already exists', success: false });
    }

    const user = await User.create({
      username,
      email,
    });

    return res
      .status(201)
      .json({ message: 'User created successfully', success: true, user });
  } catch (error) {
    console.log(error);
  }
};
export const allUsers = async (req, res) => {
  try {
    const users = await User.find({});
    return res
      .status(200)
      .json({ message: 'Users fetched successfully', success: true, users });
  } catch (error) {
    console.log(error);
    return res.status(500).json({ message: 'Internal server error' });
  }
};

Enter fullscreen mode Exit fullscreen mode
  • Inside route / userRoute.js all the user routes are written. Since I'm only using sockets to see how many users are online, there are just 2 routes here, you can add more routes if you want.
import express from 'express';
import { allUsers, register } from '../controller/userController.js';

const router = express.Router();
router.post('/register', register);
router.get('/all', allUsers);

export default router;

Enter fullscreen mode Exit fullscreen mode
  • Inside config / db.js here is the function to connect the mongoose database.
import mongoose from 'mongoose';
const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URI);
    console.log(`MongoDB connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
};
export default connectDB;
Enter fullscreen mode Exit fullscreen mode
  • Inside.env contains port numbers, client URLs, and database URIs for use in different files.
MONGO_URI=mongodb+srv://yourdbusername:password@cluster0.dfl8qwc.mongodb.net/socketio?retryWrites=true&w=majority&appName=Cluster0
PORT=5000
CLIENT_URL=http://localhost:5173
Enter fullscreen mode Exit fullscreen mode

Create Front end:

cd ..
cd client
Enter fullscreen mode Exit fullscreen mode

Create VIte and setup tailwind:

npm create vite@latest
npm i axios for data fetching from backend
npm i socket.io-client for initialize socket in fontend
npm install tailwindcss @tailwindcss/vite  for styling UI
Enter fullscreen mode Exit fullscreen mode
  • Inside vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
  plugins: [react(), tailwindcss()],
});
Enter fullscreen mode Exit fullscreen mode
  • Inside index.css
@import "tailwindcss";
npm run dev
Enter fullscreen mode Exit fullscreen mode
  • Now inside components folder write Home.jsx file. There are two jobs here. One is to send the data to the server and the other is to render here if there is a user and also show how many online users are there. To see the online users, you have to match the ID of each user using the find method with the online user array.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Home = ({ userInfo, setUser, onlineUsers }) => {
  const [formData, setFormData] = useState({ email: '', username: '' });
  const [users, setUsers] = useState([]);

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const res = await axios.post(
      'http://localhost:5000/api/users/register',
      formData
    );
    if (res?.data?.success) {
      localStorage.setItem('user', JSON.stringify(res.data.user));
      setUser(res.data.user);
      fetchAllUsers();
    }
  };
  useEffect(() => {
    fetchAllUsers();
  }, [onlineUsers]);
  const fetchAllUsers = async () => {
    try {
      const response = await axios.get('http://localhost:5000/api/users/all');
      setUsers(response?.data?.users);
    } catch (error) {
      console.error('Error fetching users:', error);
    }
  };

  if (userInfo) {
    return (
      <div className='min-h-screen flex items-center justify-center bg-gradient-to-r from-gray-800 to-black text-white'>
        <div className='bg-gray-900 rounded-xl p-8 shadow-lg w-[90%] max-w-md text-center'>
          <h1 className='text-3xl font-bold mb-4'>
            Welcome, {userInfo.username} 👋
          </h1>
          <div className='mt-10 text-left'>
            <h2 className='text-xl font-semibold mb-4'>All Users</h2>
            <ul className='space-y-3'>
              {users.map((u, index) => {
                const isOnline = onlineUsers.find(
                  (onlineUser) => onlineUser === u._id
                );
                return (
                  <li
                    key={index}
                    className='flex items-center justify-between bg-gray-800 p-3 rounded-lg'
                  >
                    <div>
                      <p className='font-medium'>{u.username}</p>
                      <p className='text-sm text-gray-400'>{u.email}</p>
                    </div>
                    <span
                      className={`w-3 h-3 rounded-full ${
                        isOnline ? 'bg-green-400' : 'bg-gray-500'
                      }`}
                      title={isOnline ? 'Online' : 'Offline'}
                    />
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className='min-h-screen flex items-center justify-center bg-gradient-to-r from-gray-800 to-black text-white'>
      <div className='bg-gray-900 rounded-xl p-8 shadow-lg w-[90%] max-w-md'>
        <h2 className='text-2xl font-bold mb-6 text-center'>Register</h2>
        <form onSubmit={handleSubmit} className='space-y-4'>
          <div>
            <label htmlFor='username' className='block text-sm mb-1'>
              Username
            </label>
            <input
              type='text'
              name='username'
              id='username'
              value={formData.username}
              onChange={handleChange}
              required
              className='w-full px-4 py-2 rounded bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-green-400'
            />
          </div>
          <div>
            <label htmlFor='email' className='block text-sm mb-1'>
              Email
            </label>
            <input
              type='email'
              name='email'
              id='email'
              value={formData.email}
              onChange={handleChange}
              required
              className='w-full px-4 py-2 rounded bg-gray-800 text-white border border-gray-600 focus:outline-none focus:ring-2 focus:ring-green-400'
            />
          </div>
          <button
            type='submit'
            className='w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded transition-all'
          >
            Register
          </button>
        </form>
      </div>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode
  • inside the src folder create a file named App.jsx and write the following code. This is where the socket is actually initialized in the frontend. At the same time it checks from localStorage whether a user exists. If a user exists then The user data is passed to the home page using props. After the socket is initialized, the real time event getOnlineUsers which was created on the server is received here. You can receive any event inside socket.on if you want. Here, getOnlineUsers returns an array in which the online users id is stored. With the help of this ID, you can find out who is online.
import { useEffect, useState } from 'react';
import './App.css';
import Home from './components/Home';
import { io } from 'socket.io-client';

function App() {
  const [user, setUser] = useState(() => {
    const stored = localStorage.getItem('user');
    return stored ? JSON.parse(stored) : null;
  });
  const [onlineUsers, setOnlineUsers] = useState([]);
  useEffect(() => {
    if (user) {
      const sock = io('http://localhost:5000', {
        query: { userId: user?._id },
        transports: ['websocket'],
      });
      sock.on('getOnlineUsers', (onlineUsers) => {
        setOnlineUsers(onlineUsers);
      });

      return () => {
        sock.close();
      };
    }
  }, [user]);
  return (
    <>
      <Home userInfo={user} setUser={setUser} onlineUsers={onlineUsers} />
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You can create many real time update related applications using socket.IO. Today, I have only shown how socket.IO can be used in a MERN stack application. You can add more if you want, such as receiving and sending user messages in a chat application , social media reactions etc.

Top comments (6)

Collapse
 
nevodavid profile image
Nevo David

Pretty cool seeing it all broken down like this- really helps when I’m trying to get socket stuff actually working in my own projects.

Collapse
 
tapader13 profile image
Minhaj Tapader

Appreciate it! Let me know if you need any help.

Collapse
 
dotallio profile image
Dotallio

This is a super approachable walkthrough for getting real-time features running in MERN. Have you tried adding real-time messaging or notifications on top of this yet?

Collapse
 
tapader13 profile image
Minhaj Tapader

Yes, of course! I actually built a full social media clone website where I implemented 1-to-1 real-time chat using Socket.IO.

Collapse
 
vy211 profile image
Vipin Yadav

Great

Collapse
 
tapader13 profile image
Minhaj Tapader

Thanks.