How to

I implemented the Dev Community Notification Center with React, Novu, and Websockets 🔥

No matter what application you are building, you will probably send the user notifications at some point. It can be over Emails, SMSs, Push notifications, or a Notification center like the one you have on the DEV Community. I will show you how to implement the DEV Community notification center in this article.

Nevo David
Nevo DavidAugust 29, 2022

For this demonstration, I created the DEV Community design – at least, I tried to. I will send a notification in the bell icon every time there is a new post. And in case we get more than one post in 30 seconds, I will merge them into one notification (Digest/Batch).

Notifications
You know this urge to click on the notifications button every-time there is a new notification? If you do, write me in the comments 👇

That’s actually a great way to get engagement in your app, but that’s for another day 🤯

What is Websockets (Socket.io)?

WebSockets create a connection between a client and a server, allowing them to send data both ways; client-server and server-client. Compared to HTTP, WebSockets provide a lasting bi-directional client-server connection, making it possible to send and receive messages in real-time.

In this article, I’ll use Socket.io for real-time communication because it follows the WebSocket protocol and provides excellent functionalities, such as fallback to HTTP long-polling or automatic reconnection, which enables us to build efficient real-time applications.

How to connect a React app to Socket.io 🚀

Here, we’ll set up the project environment for the DEV Community clone. You’ll also learn how to add Socket.io to a React and Node.js application and connect both development servers for real-time communication via Socket.io.

Create the project folder containing two sub-folders named client and server.

1mkdir devto-clone
2cd devto-clone
3mkdir client server

Navigate into the client folder via your terminal and create a new React.js project.

1cd client
2npx create-react-app ./

Install Socket.io client API and React Router. React Router is a JavaScript library that enables us to navigate between pages in a React application.

1npm install socket.io-client react-router-dom

Delete the redundant files such as the logo and the test files from the React app, and update the App.js file to display Hello World as below.

1function App() {
2    return (
3        <div>
4            <p>Hello World!</p>
5        </div>
6    );
7}
8export default App;

Navigate into the server folder and create a package.json file.

1cd server & npm init -y

Install Express.js, CORS, Nodemon, and Socket.io Server API.

Express.js is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains.

Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.

1npm install express cors nodemon socket.io

Create an index.js file – the entry point to the web server.

1touch index.js

Set up a simple Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.

1//index.js
2const express = require("express");
3const app = express();
4const PORT = 4000;
5
6app.use(express.urlencoded({ extended: true }));
7app.use(express.json());
8
9app.get("/api", (req, res) => {
10    res.json({
11        message: "Hello world",
12    });
13});
14
15app.listen(PORT, () => {
16    console.log(`Server listening on ${PORT}`);
17});

Import the HTTP and the CORS library to allow data transfer between the client and the server domains.

1const express = require("express");
2const app = express();
3const PORT = 4000;
4
5app.use(express.urlencoded({ extended: true }));
6app.use(express.json());
7
8//New imports
9const http = require("http").Server(app);
10const cors = require("cors");
11
12app.use(cors());
13
14app.get("/api", (req, res) => {
15    res.json({
16        message: "Hello world",
17    });
18});
19
20http.listen(PORT, () => {
21    console.log(`Server listening on ${PORT}`);
22});

Next, add Socket.io to the project to create a real-time connection. Before the app.get() block, copy the code below. Next, add Socket.io to the project to create a real-time connection. Before the app.get() block, copy the code below.

1//New imports
2.....
3const socketIO = require('socket.io')(http, {
4    cors: {
5        origin: "http://localhost:3000"
6    }
7});
8
9//Add this before the app.get() block
10socketIO.on('connection', (socket) => {
11    console.log(`⚡: ${socket.id} user just connected!`);
12    socket.on('disconnect', () => {
13      console.log('🔥: A user disconnected');
14    });
15});

From the code snippet above, the socket.io("connection") function establishes a connection with the React app, then creates a unique ID for each socket and logs the ID to the console whenever a user visits the web page.

When you refresh or close the web page, the socket fires the disconnect event showing that a user has disconnected from the socket.

Configure Nodemon by adding the start command to the list of scripts in the package.json file. The code snippet below starts the server using Nodemon.

1//In server/package.json
2
3"scripts": {
4    "test": "echo \"Error: no test specified\" && exit 1",
5    "start": "nodemon index.js"
6  },

You can now run the server with Nodemon by using the command below.

1npm start

Open the App.js file in the client folder and connect the React app to the Socket.io server.

1import socketIO from "socket.io-client";
2const socket = socketIO.connect("http://localhost:4000");
3
4function App() {
5    return (
6        <div>
7            <p>Hello World!</p>
8        </div>
9    );
10}
11export default App;

Start the React.js server.

1npm start

Check the terminal where the server is running; the ID of the React.js client should appear on the terminal.

Congratulations 🥂 , the React app has been successfully connected to the server via Socket.io.

💡 For the remaining part of this article, I will walk you through creating the pages for the application. We’ll create a Home page – where users sign in to the application, a 404 page for unauthenticated users, and a protected Post page only visible to authenticated users where they can create, view, and react to posts.

Creating the home page of the application

Here, we’ll create the home page for the application that accepts the username and saves it to the local storage for identification.

Create a folder named components within the client/src folder. Then, create the Home page component.

1cd src
2mkdir components & cd components
3touch Home.js

Copy the code below into the Home.js file. The code snippet displays a form input that accepts the username and stores it in the local storage.

1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Home = () => {
5    const [username, setUsername] = useState("");
6    const navigate = useNavigate();
7
8    const handleSignIn = (e) => {
9        e.preventDefault();
10        localStorage.setItem("_username", username);
11        setUsername("");
12        navigate("/post");
13    };
14    return (
15        <main className='home'>
16            <h2>Sign in to Dev.to</h2>
17            <form className='home__form' onSubmit={handleSignIn}>
18                <label htmlFor='username'>Your Username</label>
19                <input
20                    type='text'
21                    id='username'
22                    name='username'
23                    value={username}
24                    onChange={(e) => setUsername(e.target.value)}
25                />
26                <button className='home__cta'>SIGN IN</button>
27            </form>
28        </main>
29    );
30};
31
32export default Home;

Configure React Router to enable navigation between the pages of the application. Copy the code below into the src/App.js file and create the referenced components.

1import React from "react";
2import { BrowserRouter, Route, Routes } from "react-router-dom";
3import socketIO from "socket.io-client";
4
5import PostPage from "./components/PostPage";
6import Home from "./components/Home";
7import NullPage from "./components/NullPage";
8
9const socket = socketIO.connect("http://localhost:4000");
10
11const App = () => {
12    return (
13        <BrowserRouter>
14            <div>
15                <Routes>
16                    <Route path='/post' element={<PostPage socket={socket} />} />
17                    <Route path='/' element={<Home />} />
18                    <Route path='*' element={<NullPage />} />
19                </Routes>
20            </div>
21        </BrowserRouter>
22    );
23};
24
25export default App;

The code snippet assigns different routes to the Home, Post page, and Null page using React Router v6 and passes the Socket.io library into the PostPage component.

Navigate into the src/index.css file and copy the code below. It contains all the CSS required for styling this project.

1@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
2* {
3    box-sizing: border-box;
4    font-family: "Space Grotesk", sans-serif;
5}
6
7body {
8    margin: 0;
9    padding: 0;
10}
11.home {
12    width: 100%;
13    min-height: 100vh;
14    background-color: #cfd2cf;
15    display: flex;
16    flex-direction: column;
17    align-items: center;
18    justify-content: center;
19}
20.home__form {
21    display: flex;
22    flex-direction: column;
23    width: 60%;
24}
25.home__cta {
26    padding: 10px;
27    font-size: 16px;
28    cursor: pointer;
29    border: 1px solid #333;
30    outline: none;
31    width: 200px;
32}
33.navbar {
34    display: flex;
35    justify-content: space-between;
36    padding: 20px;
37    align-items: center;
38    height: 10vh;
39    position: sticky;
40    top: 0;
41    border-bottom: 1px solid #ddd;
42}
43.logo {
44    height: 7vh;
45    width: 50px;
46    border-radius: 5px;
47}
48.notification__container {
49    display: flex;
50    align-items: center;
51}
52.notification__container > div {
53    margin-right: 15px;
54}
55.logOutBtn {
56    padding: 10px;
57    width: 150px;
58    color: red;
59    border: 1px solid #333;
60    background-color: #fff;
61    cursor: pointer;
62}
63.input__container {
64    width: 100%;
65    min-height: 50vh;
66    padding: 15px;
67}
68.input__form {
69    display: flex;
70    width: 70%;
71    flex-direction: column;
72    margin: 0 auto;
73}
74label {
75    font-size: 18px;
76    margin-bottom: 10px;
77}
78input {
79    margin-bottom: 15px;
80    padding: 10px;
81    font-size: 16px;
82    outline: none;
83    border: 1px solid #ddd;
84}
85textarea {
86    font-size: 16px;
87    padding: 10px;
88    width: 100%;
89    border-radius: 10px;
90    outline: none;
91    border: 1px solid #ddd;
92    margin-bottom: 15px;
93}
94.sendBtn {
95    padding: 10px;
96    font-size: 16px;
97    height: 45px;
98    width: 200px;
99    cursor: pointer;
100    border: 1px solid #333;
101    outline: none;
102}
103.sendBtn:hover {
104    color: #fff;
105    background-color: #333;
106}
107.articles__container {
108    width: 100%;
109    padding: 20px;
110    display: flex;
111    align-items: center;
112    justify-content: center;
113    flex-direction: column;
114}
115.article {
116    width: 70%;
117    min-height: 300px;
118    padding: 15px;
119    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 1px 1px 3px 1px rgba(208, 213, 219, 0.28);
120    margin-bottom: 30px;
121    background-color: #fcfffc;
122}
123.article__content {
124    word-spacing: 3px;
125}
126.likeBtn__container {
127    display: flex;
128    align-items: center;
129}
130.likeBtn {
131    color: #fff;
132    cursor: pointer;
133    font-size: 30px;
134    margin-right: 10px;
135}
136@media screen and (max-width: 768px) {
137    .article {
138        width: 100%;
139    }
140    .input__form {
141        width: 100%;
142    }
143}

We’ve created the home page of our DEV Community clone. Next, let’s design the user interface for the post route.

Creating the 404 page for unauthenticated users

In this section, we’ll create a simple 404 page for unauthenticated users or users who are not on any of the defined routes of the application.

Navigate into the components/NullPage.js and paste the code below:

1import React from "react";
2import { Link } from "react-router-dom";
3
4const NullPage = () => {
5    return (
6        <div style={{ padding: "20px" }}>
7            <h3>
8                Seems you are lost, head back to the <Link to='/'>home page</Link>
9            </h3>
10        </div>
11    );
12};
13
14export default NullPage;

Creating the protected Post page

In this section, we’ll create the PostPage and make it visible to authenticated users only. Users can create, view, and react to posts and get notified when a user creates a post.

PostPage

From the image above, the Post page is divided into three sections:

  • Nav component – containing the DEV Community logo, the bell icon, and the logout button
  • CreatePost component – containing the form inputs, and the button
  • Post component containing the already created posts.

Since we’ve been able to define the layout for the Post page, we can now create the components for the design.

Copy the code below into the PostPage.js file. You will need to create Nav, CreatePost, and Posts components.

1import React from "react";
2import CreatePost from "./CreatePost";
3import Nav from "./Nav";
4import Posts from "./Posts";
5import NullPage from "./NullPage";
6
7const PostPage = () => {
8    return (
9        <div>
10            {localStorage.getItem("_username") ? (
11                <>
12                    <Nav />
13                    <CreatePost />
14                    <Posts />
15                </>
16            ) : (
17                <NullPage />
18            )}
19        </div>
20    );
21};
22
23export default PostPage;

The code snippet above checks if the user is signed-in before displaying the contents of the Post page; otherwise, it renders the 404 page (Null Page).

Building the Nav component

Copy the code below into the Nav.js file.

1import React from "react";
2import { useNavigate } from "react-router-dom";
3
4const Nav = () => {
5    const navigate = useNavigate();
6
7    const handleLogOut = () => {
8        localStorage.removeItem("_username");
9        navigate("/");
10    };
11
12    return (
13        <nav className='navbar'>
14            <div>
15                <img
16                    src='https://res.cloudinary.com/practicaldev/image/fetch/s--R9qwOwpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/78hs31fax49uwy6kbxyw.png'
17                    alt='Dev.to'
18                    className='logo'
19                />
20            </div>
21            <div className='notification__container'>
22                <div>
23                    <button>BELL ICON </button>
24                </div>
25                <button className='logOutBtn' onClick={handleLogOut}>
26                    LOG OUT
27                </button>
28            </div>
29        </nav>
30    );
31};
32
33export default Nav;

The code snippet above displays the DEV Community logo and two buttons representing the logout and the notification icon. The logout button signs users out and redirects them to the home page.

Building the CreatePost component

Here, we’ll create the form that allows users to create blog posts. The code snippet below accepts the title and content of the blog post and logs them to the console.

1import React, { useState } from "react";
2
3const CreatePost = () => {
4    const [title, setTitle] = useState("");
5    const [content, setContent] = useState("");
6
7    function addNewPost(e) {
8        e.preventDefault();
9        console.log({ title, content });
10        setContent("");
11        setTitle("");
12    }
13
14    return (
15        <div className='input__container'>
16            <form className='input__form' onSubmit={addNewPost}>
17                <label htmlFor='title'>Title</label>
18                <input
19                    name='title'
20                    type='text'
21                    id='title'
22                    value={title}
23                    onChange={(e) => setTitle(e.target.value)}
24                    required
25                />
26
27                <textarea
28                    name='content'
29                    id='content'
30                    rows='7'
31                    placeholder='Write the contents'
32                    value={content}
33                    required
34                    onChange={(e) => setContent(e.target.value)}
35                ></textarea>
36                <div>
37                    <button className='sendBtn'>SEND POST</button>
38                </div>
39            </form>
40        </div>
41    );
42};
43
44export default CreatePost;

Building the Posts component

Copy the code below. The blog posts will be dummy posts for now.

1import React from "react";
2
3const Posts = () => {
4    const posts = [
5        {
6            id: 1,
7            title: "What is Novu?",
8            content:
9                "is the first open-source notification infrastructure that manages all forms of communication from email to SMS, Push notifications, etc.",
10        },
11        {
12            id: 2,
13            title: "What is Websocket?",
14            content:
15                "WebSockets are used to create a connection between a client and a server, allowing them to send data both ways; client-server and server-client.",
16        },
17    ];
18    return (
19        <div className='articles__container'>
20            <h1>Recent Articles</h1>
21
22            {posts.map((post) => (
23                <div className='article' key={post.id}>
24                    <h2>{post.title}</h2>
25                    <p className='article__content'>{post.content}</p>
26                    <div className='likeBtn__container'>
27                        <p className='likeBtn'>
28                            <span role='img' aria-label='like'>
29                                👍
30                            </span>
31                        </p>
32                        <p>1</p>
33                    </div>
34                </div>
35            ))}
36        </div>
37    );
38};
39
40export default Posts;

Congratulations!💃🏻 We’ve completed the user interface for the DEV Community clone. Next, let’s create the required functionalities.

How to communicate between the React app and Socket.io server

In this section, you’ll learn how to send messages from the React app to the Node.js server and vice-versa via Socket.io.

From the App.js file, pass Socket.io down into the PostPage component – where will be communicating with the server via web sockets.

1import React from "react";
2import socketIO from "socket.io-client";
3import PostPage from "./components/PostPage";
4import { Route, Routes } from "react-router-dom";
5import Home from "./components/Home";
6import NullPage from "./components/NullPage";
7
8const socket = socketIO.connect("http://localhost:4000");
9
10const App = () => {
11    return (
12        <div>
13            <Routes>
14                <Route path='/post' element={<PostPage socket={socket} />} />
15                <Route path='/' element={<Home />} />
16                <Route path='*' element={<NullPage />} />
17            </Routes>
18        </div>
19    );
20};
21
22export default App;

Creating blog posts

Here, we will make it possible for users to create blog posts.

Update the PostPage.js file to accept Socket.io as a prop and pass it into the CreatePost component.

1import React from "react";
2import CreatePost from "./CreatePost";
3import Nav from "./Nav";
4import Posts from "./Posts";
5import NullPage from "./NullPage";
6
7const PostPage = ({ socket }) => {
8    return (
9        <div>
10            {localStorage.getItem("_username") ? (
11                <>
12                    <Nav />
13                    <CreatePost socket={socket} />
14                    <Posts />
15                </>
16            ) : (
17                <NullPage />
18            )}
19        </div>
20    );
21};
22
23export default PostPage;

Update the CreatePost component to send the posts to the backend Node.js server.

1import React, { useState } from "react";
2
3const CreatePost = ({ socket }) => {
4    const [title, setTitle] = useState("");
5    const [content, setContent] = useState("");
6
7    const addNewPost = (e) => {
8        e.preventDefault();
9//sends the post details to the backend via Socket.io
10        socket.emit("newPost", {
11            id: Math.random(),
12            title,
13            content,
14            likes: 0,
15            username: localStorage.getItem("_username"),
16        });
17
18        setContent("");
19        setTitle("");
20    };
21
22    return (
23        <div className='input__container'>
24            <form className='input__form' onSubmit={addNewPost}>
25                <label htmlFor='title'>Title</label>
26                <input
27                    name='title'
28                    type='text'
29                    id='title'
30                    value={title}
31                    onChange={(e) => setTitle(e.target.value)}
32                    required
33                />
34
35                <textarea
36                    name='content'
37                    id='content'
38                    rows='7'
39                    placeholder='Write the contents'
40                    value={content}
41                    required
42                    onChange={(e) => setContent(e.target.value)}
43                ></textarea>
44                <div>
45                    <button className='sendBtn'>SEND POST</button>
46                </div>
47            </form>
48        </div>
49    );
50};
51
52export default CreatePost;

From the code snippet above, the addNewPost function emits a message labeled newPost containing a random id, the title, content, username, and the initial number of likes for the post.

Create the event listener on the backend by copying the code below within the Socket.io block

1socketIO.on("connection", (socket) => {
2    console.log(`⚡: ${socket.id} user just connected!`);
3
4    //Event Listener for new posts
5    socket.on("newPost", (data) => {
6        //logs the newly created posts to the terminal
7        console.log(data);
8    });
9
10    socket.on("disconnect", () => {
11        socket.disconnect();
12    });
13});

Displaying the blog posts to users

In the previous section, we successfully sent the posts to the backend. Here, we will send the posts back to the React app for display.

Create an array in the server/index.js and add the newly created posts to the array.

1let posts = [];
2
3socketIO.on("connection", (socket) => {
4    console.log(`⚡: ${socket.id} user just connected!`);
5
6    //Event Listener for new posts
7    socket.on("newPost", (data) => {
8        //adds every new post as the first element in the array
9        posts.unshift(data);
10    });
11
12    socket.on("disconnect", () => {
13        socket.disconnect();
14    });
15});

Create another event that sends the array of posts to the React app.

1let posts = [];
2
3socketIO.on("connection", (socket) => {
4    console.log(`⚡: ${socket.id} user just connected!`);
5
6    //Event Listener for new posts
7    socket.on("newPost", (data) => {
8        //adds every new post as the first element in the array
9        posts.unshift(data);
10        //sends the array of posts to the React app
11        socket.emit("posts", posts);
12    });
13
14    socket.on("disconnect", () => {
15        socket.disconnect();
16    });
17});

Head back to the PostPage.js file, create a listener to the “post” event, and pass the details into the Posts component for display.

1import React, { useState, useEffect } from "react";
2import CreatePost from "./CreatePost";
3import Nav from "./Nav";
4import Posts from "./Posts";
5import NullPage from "./NullPage";
6
7const PostPage = ({ socket }) => {
8    const [posts, setPosts] = useState([]);
9
10    useEffect(() => {
11        socket.on("posts", (data) => setPosts(data));
12    }, []);
13
14    return (
15        <div>
16            {localStorage.getItem("_username") ? (
17                <>
18                    <Nav />
19                    <CreatePost socket={socket} />
20                    <Posts posts={posts} />
21                </>
22            ) : (
23                <NullPage />
24            )}
25        </div>
26    );
27};
28
29export default PostPage;

Render the posts via the Posts component. The code snippet below checks if the post array is not empty before passing the data into the user interface.

1import React from "react";
2
3const Posts = ({ posts }) => {
4    return (
5        <div className='articles__container'>
6            {posts[0] && <h1>Recent Articles</h1>}
7            {posts.length > 0 &&
8                posts.map((post) => (
9                    <div className='article' key={post.id}>
10                        <h2>{post.title}</h2>
11                        <p className='article__content'>{post.content}</p>
12                        <div className='likeBtn__container'>
13                            <p className='likeBtn'>
14                                <span role='img' aria-label='like'>
15                                    👍
16                                </span>
17                            </p>
18                            <p>{post.likes}</p>
19                        </div>
20                    </div>
21                ))}
22        </div>
23    );
24};
25
26export default Posts;

Next, let’s enable users to like their favorite posts and update the number of likes accordingly.

Creating the “like post” functionality

In this section, I’ll walk you through adding the “like post” functionality to the application, enabling users to react to any post of their choice.

Pass Socket.io into the Posts component and create a postLiked function which triggers an event that sends the id of the post liked by the user to the Node.js server.

1import React from "react";
2
3const Posts = ({ posts, socket }) => {
4    //Sends the id of the selected post via a Socket.io event
5    const postLiked = (id) => socket.emit("postLiked", id);
6
7    return (
8        <div className='articles__container'>
9            {posts[0] && <h1>Recent Articles</h1>}
10            {posts.length > 0 &&
11                posts.map((post) => (
12                    <div className='article' key={post.id}>
13                        <h2>{post.title}</h2>
14                        <p className='article__content'>{post.content}</p>
15                        <div className='likeBtn__container'>
16
17                            {/* The postLiked function runs after clicking on the like emoji*/}
18                            <p className='likeBtn' onClick={() => postLiked(post.id)}>
19                                <span role='img' aria-label='like'>
20                                    👍
21                                </span>
22                            </p>
23                            <p>{post.likes > 0 && post.likes}</p>
24
25                        </div>
26                    </div>
27                ))}
28        </div>
29    );
30};
31
32export default Posts;

Create a listener on the Node.js server that accepts the post id, updates the number of likes, and sends the post with its newly updated likes count back to the React app.

1/*
2The increaseLikes function loops through the array of posts,
3fetches for a post with the same ID, and
4updates the number of likes
5*/
6const increaseLikes = (postId, array) => {
7    for (let i = 0; i < array.length; i++) {
8        if (array[i].id === postId) {
9            array[i].likes += 1;
10        }
11    }
12};
13
14socketIO.on("connection", (socket) => {
15    console.log(`⚡: ${socket.id} user just connected!`);
16
17    socket.on("newPost", (data) => {
18        posts.unshift(data);
19        socket.emit("posts", posts);
20    });
21    socket.on("postLiked", (postId) => {
22        //Function accepts the post ID and post array
23        increaseLikes(postId, posts);
24        //Sends the newly updated array to the React app
25        socket.emit("posts", posts);
26    });
27    socket.on("disconnect", () => {
28        socket.disconnect();
29    });
30});

Congratulations!🔥🎉 The application is almost complete.

We’ve been able to able set up the communication channels between the React app and the Node.js server. Next, let’s learn how to display notifications to the users when a new blog post is created or liked.

How to add Novu to a React & Node.js app

In this section, you’ll learn how to add Novu to the DEV Community clone to enable us to send notifications from the React app to the Socket.io server.

Novu – the first open-source notification infrastructure

Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community – Websockets), Emails, SMSs and so on.

We are putting on awesome stuff every week!
Follow us on Twitter so you can get things we don’t put here!
https://twitter.com/novuhq

Novu

Navigate into the client folder and create a Novu project by running the code below.

cd client
npx novu init

You will need to sign in with Github before creating a Novu project. The code snippet below contains the steps you should follow after running npx novu init.

Now let's setup your account and send your first notification
❓ What is your application name? Devto Clone
❓ Now lets setup your environment. How would you like to proceed?
   > Create a free cloud account (Recommended)
❓ Create your account with:
   > Sign-in with GitHub
❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
    > Yes
✔️ Create your account successfully.

We've created a demo web page for you to see novu notifications in action.
Visit: http://localhost:57807/demo to continue

Visit the demo web page http://localhost:57807/demo, copy your subscriber ID from the page, and click the Skip Tutorial button. We’ll be using it later in this tutorial.

Demo

Install Novu Notification package as a dependency in your React project.

1npm install @novu/notification-center

Update the components/Nav.js file to contain Novu and its required elements from the documentation.

1import React from "react";
2import {
3    NovuProvider,
4    PopoverNotificationCenter,
5    NotificationBell,
6} from "@novu/notification-center";
7import { useNavigate } from "react-router-dom";
8
9const Nav = () => {
10    const navigate = useNavigate();
11
12    const onNotificationClick = (notification) =>
13        navigate(notification.cta.data.url);
14
15    const handleLogOut = () => {
16        localStorage.removeItem("_username");
17        navigate("/");
18    };
19
20    return (
21        <nav className='navbar'>
22            <div>
23                <img
24                    src='https://res.cloudinary.com/practicaldev/image/fetch/s--R9qwOwpC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/78hs31fax49uwy6kbxyw.png'
25                    alt='Dev.to'
26                    className='logo'
27                />
28            </div>
29            <div className='notification__container'>
30                <div>
31                    <NovuProvider
32                        subscriberId='<YOUR_SUBSCRIBER_ID>'
33                        applicationIdentifier='<YOUR_APP_ID>'
34                    >
35                        <PopoverNotificationCenter
36                            onNotificationClick={onNotificationClick}
37                            colorScheme='light'
38                        >
39                            {({ unseenCount }) => (
40                                <NotificationBell unseenCount={unseenCount} />
41                            )}
42                        </PopoverNotificationCenter>
43                    </NovuProvider>
44                </div>
45                <button className='logOutBtn' onClick={handleLogOut}>
46                    LOG OUT
47                </button>
48            </div>
49        </nav>
50    );
51};
52
53export default Nav;

The code snippet above adds Novu’s notification bell icon to the Nav component, enabling us to view all the notifications in our app.

💡 The NovuProvider component requires your Subscriber ID – copied earlier from http://localhost:57807/demo and your application ID available in the Settings section under API Keys on the Novu Manage Platform.

NovuProvider

Navigate into the server folder and install the Novu SDK for Node.js.

1cd server
2npm install @novu/node

Import Novu from the package and create an instance using your API Key.

1//server/index.js
2
3const { Novu } = require("@novu/node");
4const novu = new Novu("<YOUR_API_KEY>");
5
Import Novu

Create a new POST route on the server – from which Novu will send the notifications.

1app.post("/notify", async (req, res) => {
2    const { username } = req.body;
3    console.log({ username });
4});

Sending in-app notifications with Novu

In the previous section, I walked you through setting up Novu on the React and Node.js server. Here, I will guide you through sending notifications via Novu in your web application.

Open the Novu Manage Platform in your browser; a notification template is in the Notifications tab.

Select the template, click on Workflow Editor, and ensure the workflow is as below:

Novu Digest allows you to control how you send notifications in your app. It collects multiple trigger events and sends them as a single message.

Click the In-App step and edit the template to contain the content below

{{username}} just added a new post!

💡 Novu allows you to add dynamic content or data to the templates using the Handlebars templating engine. The data for the username variable will be passed into the template when making the POST request to Novu.

Save the template by clicking Update button, then head back to your code editor, and update the POST route from the previous section as below:

1app.post("/notify", async (req, res) => {
2    const { username } = req.body;
3    await novu
4        .trigger("<NOTIFICATION_TEMPLATE_IDENTIFIER>", {
5            to: {
6                subscriberId: "<YOUR_SUBSCRIBER_ID>",
7            },
8            payload: {
9                username,
10            },
11        })
12        .catch((err) => console.error(err));
13});

The code snippet above triggers the notification template via its ID and also provides the required data – username as a payload to the template.

Having configured Novu on the server and created the route for sending the notifications, send a request to fetch the notifications on the frontend.

Update the CreatePost.js file as below:

1import React, { useState } from "react";
2
3const CreatePost = ({ socket }) => {
4    const [title, setTitle] = useState("");
5    const [content, setContent] = useState("");
6
7    function addNewPost(e) {
8        e.preventDefault();
9        socket.emit("newPost", {
10            id: Math.random(),
11            title,
12            content,
13            likes: 0,
14            username: localStorage.getItem("_username"),
15        });
16        /* 
17            Calls the sendNotification function immediately 
18            after creating a new post 
19        */
20        sendNotification();
21        setContent("");
22        setTitle("");
23    }
24
25    /*
26        The sendNotification function makes a post request to the server
27        containing the username saved in the local storage.
28    */
29    async function sendNotification() {
30        try {
31            const sendNotification = await fetch("http://localhost:4000/notify", {
32                method: "POST",
33                body: JSON.stringify({
34                    username: localStorage.getItem("_username"),
35                }),
36                headers: {
37                    Accept: "application/json",
38                    "Content-Type": "application/json",
39                },
40            });
41            const data = await sendNotification.json();
42            console.log(data);
43        } catch (err) {
44            console.error(err);
45        }
46    }
47
48    return (
49        <div className='input__container'>
50            <form className='input__form' onSubmit={addNewPost}>
51                ...
52            </form>
53        </div>
54    );
55};
56
57export default CreatePost;

Congratulations! 💃🏻 We’ve completed the code for this project. You can view the notifications by clicking on the notification bell in the nav bar.

Congrats

Conclusion

So far, you’ve learnt how to add Novu to a React and Node.js application, send notifications with Novu, set up Socket.io in a React and Node.js application, and send messages between the client and a Node.js server.

This article demonstrates what you can build using Socket.io and Novu. Feel free to improve on the project by:

  • adding an authentication library
  • saving the blog posts to a database that supports real-time communication
  • adding the ability to comment on each blog post
  • sending notifications via Novu when a user reacts and comments on a blog post.

The complete code for this tutorial is available here: https://github.com/novuhq/blog/tree/main/devto-notifications-novu

Thank you for reading!

P.S We are putting on awesome stuff every week!
Follow us on Twitter so you can get things we don’t put here!
https://twitter.com/novuhq

Twitter

Nevo David
Nevo DavidAugust 29, 2022

Related Posts

How to

The Ultimate Guide to Laravel Reverb: Real-Time Notifications

You learned a lot about using Laravel Reverb in the first part of this guide. Now, you’ll learn how to add real-time notifications seamlessly to your Laravel apps.

Prosper Otemuyiwa
Prosper OtemuyiwaApril 9, 2024
How to

The Ultimate Guide to Laravel Reverb

Learn how to use Laravel Reverb to develop a real-time Laravel app. You'll learn about channels, events, broadcasting, authorization and configuring Laravel Reverb WebSocket Server connections.

Prosper Otemuyiwa
Prosper OtemuyiwaApril 5, 2024
How to

Building an Investor List App with Novu and Supabase

Building an Investor List App with Novu and Supabase

Prosper Otemuyiwa
Prosper OtemuyiwaMarch 15, 2024