How can I implement real time changes between my frontend and the database.
Tags: express, mongodb, react, javascript
Published: 2024-02-10 20:13:20

## **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`.

```javascript
 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.
```javascript

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

```javascript
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

```javascript
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;
```
 

```javascript
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

```javascript
let ioInstance;

function setIo(io) {
  if (!ioInstance) {
    ioInstance = io;
  }
}

function getIo() {
  return ioInstance;
}

module.exports = {
  setIo,
  getIo,
  ioInstance,
};
```
**_5.2_**-update index.js file 

```javascript
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

```javascript
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 

```javascript
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

```javascript
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 

```javascript
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.