Introduction
Developers often encounter challenges when building apps that require real time communication.
In this blog post, we'll explore the concept of a real-time communication and its critical role in delivering a seamless user experience.
lets take examples of some real scenarios when the user needs the most up to date data from the server or more specifically from the database.
lets take the example of e-commerce web site when the user is adding items to his cart-item, meanwhile a thousands of other users performing the same process, however some items with a limit quantities , what could happen in this case ?
of course the user(s) will fail to get a positive response from the server and they can not realize that only if they reload their browser constantly which is lead to a bad user experience.
Or consider a renting car platform, a user try to rent a car, and others choose the same one at the same time.
Until this point, it is clear how these scenarios are represent a critical challenge .
So how can we address this issue without instructing the client to keep loading his page?
From this needs sorted out the idea of real time communication between the server and the frontend or the real time database (which is another process could be discussed in other tutorials).
Socket.IO
We will not delve deeply into defining Socket.IO in this tutorial. What you should grasp for the moment is that Socket.IO
is a JavaScript library that facilitates real-time, bidirectional communication between web clients and servers. Developers often associate Socket.IO with chat apps. However, in this tutorial, we will not focus on that scenario. Instead, our emphasis will be on a simple use case—implementing it in a mini blog app between Express.js and React and see how after a post request from any user all others will see changes immediately in their browsers.
Tutorial
The primary focus of this tutorial is to comprehensively understand the process of sending notifications to the client upon updating the database. Technically, we will emit an event containing the updated data and listen for it on the frontend. This ensures that with each HTTP request, the browser automatically updates the view, providing a seamless experience for the user.
the tuto example scenario: A mini blog-app application.
Prerequisites
Before diving into the tutorial, ensure you have the following prerequisites:
-A basic understanding of react
, express.js
and mongoDB
.
-Node.js
installed on your system.
-A code editor of your choice, preferred Visual Studio Code
.
Integrate Socket.IO
with express
1. make sure that you are in the right directory then init your express project by running the below command in your terminal:
npm init -y
2.install express
, Socket.IO
, CORS
middleware, mongoDB
and dotenv
for environment variables
npm install express cors socket.io dotenv mongoose
3.create an index.js file in your directory and copy the code below, don't be confused, you can refer to the documentation of socket.Io to find how to integrate it in your index.js https://socket.io/docs/v4/tutorial/step-3
.
const express = require("express");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const cors = require("cors");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3500;
const dbConnect = require("./db/DbConnect");
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
//in this case the react app run on port 5173
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
app.use(express.json());
app.use(cors());
io.on("connection", (socket) => {
console.log(socket.id);
});
httpServer.listen(PORT, () => {
console.log(`server is running on port ${PORT}`);
dbConnect();
});
-I initialize a new instance of socket.io by passing the server (the HTTP server) object. Then I listen on the connection event for incoming sockets and log it to the console.
-the call of the function dbConnect
is to ensure connection with the database , if you are not familiar on how to create a connection with mongoDB database , create a file DbConnect
and copy the code below.
const mongoose = require("mongoose");
require("dotenv").config();
function connectDB() {
mongoose
.connect(process.env.DBURI) // replace this with your .env URI
.then(() => console.log("db connected"))
.catch((err) => console.log(err.message));
}
module.exports = connectDB;
4.Emit an event after every HHTP post request.
4.1 update the index.js file as below
const express = require("express");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const Post = require("../db/postModel");
const User = require("../db/userModel");
const cors = require("cors");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3500;
const dbConnect = require("./db/DbConnect");
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
//in this case the react app run on port 5173
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
app.use(express.json());
app.use(cors());
io.on("connection", (socket) => {
console.log(socket.id);
});
app.post("post/:userId",async (req, res) => {
const { userId } = req.params;
const { title, content } = req.body;
try {
const newPost = new Post({ userId, title, content });
await newPost.save();
if (newPost) {
const list = await Post.find();
io.emit("post_created", list);
res.status(201).json({ message: "new post created successfully!" });
} else res.status(400).json({ message: "something went wrong" });
} catch (error) {
res
.status(error.status || 500)
.json({ Error: error.message || `internal server problem` });
}
})
httpServer.listen(PORT, () => {
console.log(`server is running on port ${PORT}`);
dbConnect();
});
In this new index.js file , I added an HTTP post request
to endpoint post/:userId
, let us understand and breakdown the code of the controller function:
-the purpose of this request is to create a new post for a specific user which his id included as params, then we get the list of all posts and include it as data to be emitted with the event called here post_created
.
I think here the process of socket.IO events become clear ,it is all about giving a name to an event and emit it with a specific data.
also if you are not familiar with mongoose and mongoDB, there were two queries in the function:
-Create new post with the query: new Post({// your_model_fields_here})
-Get posts list with the query: await Post.find()
=> After these two steps the updated list of posts is ready and could be send as data with the emitted event
-the two models imported in the file are a simple mongoose model , you can create your customize models (refering to moongoose documentation) or copy the two codes below and paste them inside two files:
db/postModel
db/userModel
const mongoose = require("mongoose");
const schema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
validate: {
validator: async function (userId) {
const user = await mongoose.model("User").findById(userId);
return user !== null;
},
message: "User with the specified ID does not exist.",
},
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
});
const Post = mongoose.model("Post", schema);
module.exports = Post;
const mongoose = require("mongoose");
const schema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
});
const User = mongoose.model("User", schema);
module.exports = User;
5.Structure of the backend folder and separate concerns
If you've already noticed, including the HTTP POST request in the index.js
file makes it too long and doesn't adhere to the separation of concerns principle.
Many of you may be familiar with the best practice of separating routes and controller functions into different folders. However, the challenge arises when dealing with the Socket.IO instance io
, which resides in the index.js file. The controller function is invoked in the routes folder rather than the server file. Even considering passing io as a parameter won't work, as it isn't directly accessible in the routes files.
For this reason, I will create an instance io in a separate file and set/get it from anywhere in the app.
5.1-create socket.js file
let ioInstance;
function setIo(io) {
if (!ioInstance) {
ioInstance = io;
}
}
function getIo() {
return ioInstance;
}
module.exports = {
setIo,
getIo,
ioInstance,
};
5.2-update index.js file
const express = require("express");
const { createServer } = require("node:http");
const { Server } = require("socket.io");
const cors = require("cors");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3500;
const dbConnect = require("./db/DbConnect");
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
const socket = require("./socket");
socket.setIo(io);
app.use(express.json());
app.use(cors());
io.on("connection", (socket) => {
console.log(socket.id);
});
app.use("/user", require("./routes/useRoute"));
app.use("/post", require("./routes/postRoute"));
httpServer.listen(PORT, () => {
console.log(`server is running on port ${PORT}`);
dbConnect();
});
In the code above I deleted the HTTP post request, imported the socket instance and set it to io
5.3-create post controller file inside a the controllers folder
controllers/postController
const Post = require("../db/postModel");
const User = require("../db/userModel");
require("dotenv").config();
const socket = require("../socket");
const io = socket.getIo();
const createPost = async (req, res) => {
const { userId } = req.params;
const { title, content } = req.body;
try {
const newPost = new Post({ userId, title, content });
await newPost.save();
if (newPost) {
const list = await Post.find();
io.emit("post_created", list);
res.status(201).json({ message: "new post created successfully!" });
} else res.status(400).json({ message: "something went wrong" });
} catch (error) {
res
.status(error.status || 500)
.json({ Error: error.message || `internal server problem` });
}
};
//other controller functions here
module.exports = {
createPost,
};
In this step I got the io
instance from the socket.js file
const io = socket.getIo()
, so i could use it in my controller function.
5.4-create post route file inside the routes folder
const express = require("express");
const router = express.Router();
const postController = require("../controllers/postController");
router.post(
"/:userId",
postController.createPost
);
//other routes here
module.exports = router;
Integrate Socket.IO
with react
The last step of the tuto is to listen to the event in the component so the browser will update the view automatically.
For our case with react
, we need a component that display posts .
In our component we will need a state : array of posts
and a useEffect hook responsible for updating the state.
_1. Integrating socket.IO with react
npm i socket.io-client
*2. * create a component Listposts
and copy the code below
import { useEffect, useState } from "react";
import io from "socket.io-client";
const socket = io("http://localhost:your_server_port");
const Listposts = () => {
const [posts, setPosts] = useState<PostType[]>([])
return(
//map your posts list here
)
export default Listposts;
3. Add a useEffect
hook to fetch data from the server and also to listen to the emitted event coming from the server.
update your component to include this logic
import { useEffect, useState } from "react";
import axios from 'axios'
import io from "socket.io-client";
const socket = io("http://localhost:your_server_port");
const Listposts = () => {
const [posts, setPosts] = useState<PostType[]>([])
useEffect(() => {
axios
.get("http://localhost:your_server_port/post")
.then((res) => setPosts(res.data.list))
.catch((err) => alert(err.message));
socket.on("post_created", (data: PostType[]) => {
setPosts(data);
});
}, [ posts]);
return(
//map your posts list here
)
export default Listposts;
Many of you are familiar with fetching the data from the server and setting the state accordingly , the new thing when implementing a real time communication is to set the state also when listenning to the event.
Note : Don't forget to test your useEffect
for any unexpected infinite loop caused by the dependency array and fix it by implementing the useCallback()
and useMemo()
.
for those who use classical component the same logique could be implemented inside the componentDidMount
method.
Conclusion
For this tutorial, the purpose was to explain the importance of implementing real-time communication in apps that require it for a better user experience.
In this case, we used the MongoDB
database, but the same logic could be applied to relational databases like MySQL
.
Finally, I would like to emphasize the distinction between real-time communication and real-time databases.
Top comments (0)