Building a Notion-like system with Socket.io And React 😍
We are going to build a knowledge system like Click-Up and Notion. You will be able to add posts, write comments, tag other users and show it in their notifications. In notion users can see what other users do in real-time without refreshing the page. This is why we will be using Socket.io
What is WebSocket?
WebSocket is a built-in Node.js module that enables us to create a real-time connection between a client and a server, allowing them to send data in both ways. However, WebSocket is low-level and doesn’t provide the functionalities required to build complex real-time applications; this is why Socket.io exists.
Socket.io is a popular JavaScript library that allows us to create real-time, bi-directional communication between software applications and a Node.js server. It is optimised to process a large volume of data with minimal delay and provides better functionalities, such as fallback to HTTP long-polling or automatic reconnection.
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.
How to create a real-time connection with React & Socket.io
Here, we’ll set up the project environment for the notion app. 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 notion-platform
2cd notion-platform
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;
Add the Socket.io client API to the React app as below:
1import { io } from "socket.io-client";
2
3//👇🏻 http://localhost:4000 is where the server host URL.
4const socket = io.connect("http://localhost:4000");
5
6function App() {
7 return (
8 <div>
9 <p>Hello World!</p>
10 </div>
11 );
12}
13export 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.
1npm install express cors nodemon socket.io
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.
Create an index.js file – the entry point to the web server.
1touch index.js
Set up a 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
5//👇🏻 New imports
6const http = require("http").Server(app);
7const cors = require("cors");
8
9app.use(express.urlencoded({ extended: true }));
10app.use(express.json());
11app.use(cors());
12
13app.get("/api", (req, res) => {
14 res.json({
15 message: "Hello world",
16 });
17});
18
19http.listen(PORT, () => {
20 console.log(`Server listening on ${PORT}`);
21});
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
13 socket.on('disconnect', () => {
14 socket.disconnect()
15 console.log('🔥: A user disconnected');
16 });
17});
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"
2scripts": {
3 "test": "echo \"Error: no test specified\" && exit 1",
4 "start": "nodemon index.js"
5 }
You can now run the server with Nodemon by using the command below.
1npm start
Building the user interface
Here, we’ll create the user interface for the notion application to enable users to sign in, write posts, add comments, and tag other users.
Navigate into the client/src
folder and create a components folder containing Login.js
, Home.js
, CreatePost.js
, and NotionPost.js
files.
1cd client
2mkdir components
3cd components
4touch Login.js Home.js CreatePost.js NotionPost.js
Update the App.js
file to render the newly created components on different routes via React Router as below:
1import { BrowserRouter, Routes, Route } from "react-router-dom";
2import NotionPost from "./components/NotionPost";
3import CreatePost from "./components/CreatePost";
4import Home from "./components/Home";
5import Login from "./components/Login";
6import { io } from "socket.io-client";
7
8const socket = io.connect("http://localhost:4000");
9
10function App() {
11 return (
12 <BrowserRouter>
13 <Routes>
14 <Route path='/' element={<Login socket={socket} />} />
15 <Route path='/dashboard' element={<Home socket={socket} />} />
16 <Route path='/post/create' element={<CreatePost socket={socket} />} />
17 <Route path='/post/:id' element={<NotionPost socket={socket} />} />
18 </Routes>
19 </BrowserRouter>
20 );
21}
22
23export default App;
Navigate into the src/index.css
file and copy the code below. It contains all the CSS required for styling this project.
The Login page
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 padding: 0;
5 margin: 0;
6 font-family: "Space Grotesk", sans-serif;
7}
8body {
9 padding: 0;
10}
11.login {
12 width: 100%;
13 min-height: 100vh;
14 display: flex;
15 flex-direction: column;
16 align-items: center;
17 justify-content: center;
18}
19.login > h2 {
20 color: #00abb3;
21 margin-bottom: 30px;
22}
23.loginForm {
24 width: 70%;
25 display: flex;
26 flex-direction: column;
27}
28.loginForm > input {
29 margin: 10px 0;
30 padding: 10px 15px;
31}
32.home__navbar {
33 width: 100%;
34 height: 10vh;
35 padding: 20px;
36 display: flex;
37 align-items: center;
38 justify-content: space-between;
39 border-bottom: 1px solid #eaeaea;
40}
41.home__navbar > h2 {
42 color: #00abb3;
43}
44.home__buttons {
45 display: flex;
46 align-items: center;
47 justify-content: baseline;
48}
49.home__createBtn {
50 padding: 10px;
51 cursor: pointer;
52 margin-right: 10px;
53 background-color: #00abb3;
54 border: none;
55 outline: none;
56 color: #fff;
57}
58.home__createBtn:hover,
59.createForm__button:hover {
60 background-color: #02595e;
61}
62.home__notifyBtn {
63 padding: 10px;
64 cursor: pointer;
65 color: #00abb3;
66 background-color: #fff;
67 border: 1px solid #3c4048;
68 outline: none;
69 width: 100px;
70}
71.posts__container {
72 width: 100%;
73 min-height: 90vh;
74 padding: 30px 20px;
75}
76.post {
77 width: 100%;
78 min-height: 8vh;
79 background-color: #00abb3;
80 border-radius: 5px;
81 padding: 20px;
82 display: flex;
83 color: #eaeaea;
84 align-items: center;
85 justify-content: space-between;
86 margin-bottom: 15px;
87}
88.post__cta {
89 padding: 10px;
90 background-color: #fff;
91 cursor: pointer;
92 outline: none;
93 border: none;
94 border-radius: 2px;
95}
96.createPost {
97 min-height: 100vh;
98 width: 100%;
99 padding: 30px 20px;
100}
101.createPost > h2 {
102 text-align: center;
103 color: #00abb3;
104}
105.createForm {
106 width: 100%;
107 min-height: 80vh;
108 padding: 20px;
109 display: flex;
110 flex-direction: column;
111}
112.createForm__title {
113 padding: 10px;
114 height: 45px;
115 margin-bottom: 10px;
116 text-transform: capitalize;
117 border: 1px solid #3c4048;
118}
119.createForm__content {
120 padding: 15px;
121 margin-bottom: 15px;
122 border: 1px solid #3c4048;
123}
124.createForm__button {
125 width: 200px;
126 padding: 10px;
127 height: 45px;
128 background-color: #00abb3;
129 color: #fff;
130 outline: none;
131 border: none;
132 cursor: pointer;
133 border-radius: 5px;
134}
135.notionPost {
136 width: 100%;
137 min-height: 100vh;
138 background-color: #eaeaea;
139 display: flex;
140 flex-direction: column;
141 padding: 30px 50px;
142}
143.notionPost__container {
144 width: 90%;
145 min-height: 70vh;
146 margin-bottom: 30px;
147}
148.notionPost__author {
149 color: #00abb3;
150}
151.notionPost__date {
152 opacity: 0.4;
153 font-size: 12px;
154}
155.notionPost__content {
156 padding-top: 30px;
157 line-height: 200%;
158}
159.comments__container {
160 min-height: 70vh;
161 border: 1px solid #3c4048;
162 padding: 30px;
163 box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 2px 2px 3px 1px rgba(208, 213, 219, 0.28);
164 border-radius: 3px;
165}
166.comments__inputContainer {
167 display: flex;
168 align-items: center;
169 margin: 30px 0;
170}
171.comments__input {
172 width: 50%;
173 padding: 15px;
174 margin-right: 15px;
175}
176.comments__cta,
177.login__cta {
178 padding: 15px;
179 width: 200px;
180 cursor: pointer;
181 outline: none;
182 border: none;
183 background-color: #00abb3;
184 color: #fff;
185}
186.comment {
187 margin-bottom: 15px;
188}
Here, the application accepts the username and saves it in the local storage for user identification. Copy the code below into the Login component.
1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Login = () => {
5 const [username, setUsername] = useState("");
6 const navigate = useNavigate();
7
8 const handleLogin = (e) => {
9 e.preventDefault();
10 //The username 👉🏻 console.log({ username });
11 localStorage.setItem("username", username);
12 navigate("/dashboard");
13 };
14 return (
15 <div className='login'>
16 <h2>Sign in to HackNotion</h2>
17 <form className='loginForm' onSubmit={handleLogin}>
18 <label htmlFor='username'>Enter your username</label>
19 <input
20 name='username'
21 id='username'
22 type='text'
23 value={username}
24 required
25 onChange={(e) => setUsername(e.target.value)}
26 />
27 <button className='login__cta'>LOG IN</button>
28 </form>
29 </div>
30 );
31};
32
33export default Login;
The Home page
Copy the code below into the Home.js
file. It represents the home layout for the application.
1import React from "react";
2import { useNavigate } from "react-router-dom";
3
4const Home = () => {
5 const navigate = useNavigate();
6
7 const createPostBtn = () => navigate("/post/create");
8 const readMoreBtn = () => navigate("/post/:id");
9
10 return (
11 <div className='home'>
12 <nav className='home__navbar'>
13 <h2>HackNotion</h2>
14 <div className='home__buttons'>
15 <button className='home__createBtn' onClick={createPostBtn}>
16 CREATE POST
17 </button>
18 <button className='home__notifyBtn'>NOTIFY</button>
19 </div>
20 </nav>
21
22 <div className='posts__container'>
23 <div className='post'>
24 <h3>How to create a new Socket.io client</h3>
25 <button className='post__cta' onClick={readMoreBtn}>
26 READ MORE
27 </button>
28 </div>
29
30 <div className='post'>
31 <h3>Creating React Native project with Expo</h3>
32 <button className='post__cta' onClick={readMoreBtn}>
33 READ MORE
34 </button>
35 </div>
36 </div>
37 </div>
38 );
39};
40
41export default Home;
The NotionPost page
This page is dynamic and displays the content of each post via the ID passed into the URL. Here, users can read the notion post and add comments.
Copy the code below into the NotionPost.js
file:
1import React, { useState } from "react";
2
3const NotionPost = () => {
4 const [comment, setComment] = useState("");
5
6 const handleAddComment = (e) => {
7 e.preventDefault();
8 console.log({ comment });
9 setComment("");
10 };
11
12 return (
13 <div className='notionPost'>
14 <div className='notionPost__container'>
15 <h1>How to create a new React Native project with Expo</h1>
16 <div className='notionPost__meta'>
17 <p className='notionPost__author'>By Nevo David</p>
18 <p className='notionPost__date'>Created on 22nd September, 2022</p>
19 </div>
20
21 <div className='notionPost__content'>
22 For this article, I will use Puppeteer and ReactJS. Puppeteer is a
23 Node.js library that automates several browser actions such as form
24 submission.
25 </div>
26 </div>
27
28 <div className='comments__container'>
29 <h2>Add Comments</h2>
30 <form className='comments__inputContainer' onSubmit={handleAddComment}>
31 <textarea
32 placeholder='Type in your comments...'
33 rows={5}
34 className='comments__input'
35 value={comment}
36 required
37 onChange={(e) => setComment(e.target.value)}
38 />
39 <button className='comments__cta'>Add Comment</button>
40 </form>
41
42 <div>
43 <p className='comment'>
44 <span style={{ fontWeight: "bold" }}>Scopsy Dima</span> - Nice post
45 fam!❤️
46 </p>
47 </div>
48 </div>
49 </div>
50 );
51};
52
53export default NotionPost;
The CreatePost page
Here, we’ll create a simple layout that allows users to create posts by adding the title and its content. Users will also be able to tag other users using React Tag.
React Tag is a library that allows us to create tags easily via a single component. It provides several features, such as autocomplete based on a suggestion list, reordering using drag-and-drop, and many more.*
Copy the code below into the CreatePost.js
file.
1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const CreatePost = () => {
5 const navigate = useNavigate();
6
7 const [postTitle, setPostTitle] = useState("");
8 const [postContent, setPostContent] = useState("");
9
10 //...gets the publish date for the post
11 const currentDate = () => {
12 const d = new Date();
13 return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`;
14 };
15
16 //...logs the post details to the console
17 const addPost = (e) => {
18 e.preventDefault();
19 console.log({
20 postTitle,
21 postContent,
22 username: localStorage.getItem("username"),
23 timestamp: currentDate(),
24 });
25 navigate("/dashboard");
26 };
27
28 return (
29 <>
30 <div className='createPost'>
31 <h2>Create a new Post</h2>
32 <form className='createForm' onSubmit={addPost}>
33 <label htmlFor='title'> Title</label>
34 <input
35 type='text'
36 required
37 value={postTitle}
38 onChange={(e) => setPostTitle(e.target.value)}
39 className='createForm__title'
40 />
41
42 <label htmlFor='title'> Content</label>
43 <textarea
44 required
45 rows={15}
46 value={postContent}
47 onChange={(e) => setPostContent(e.target.value)}
48 className='createForm__content'
49 />
50
51 <button className='createForm__button'>ADD POST</button>
52 </form>
53 </div>
54 </>
55 );
56};
57
58export default CreatePost;
Import React Tags into the CreatePost.js
file:
1import { WithContext as ReactTags } from "react-tag-input";
Update the CreatePost component to contain the code snippet below for creating tags with React Tags.
1//👇🏻 The suggestion list for autocomplete
2const suggestions = ["Tomer", "David", "Nevo"].map((name) => {
3 return {
4 id: name,
5 text: name,
6 };
7});
8
9const KeyCodes = {
10 comma: 188,
11 enter: 13,
12};
13//👇🏻 The comma and enter keys are used to separate each tags
14const delimiters = [KeyCodes.comma, KeyCodes.enter];
15
16//...The React component
17const CreatePost = () => {
18 //👇🏻 An array containing the tags
19 const [tags, setTags] = useState([]);
20
21 //...deleting tags
22 const handleDelete = (i) => {
23 setTags(tags.filter((tag, index) => index !== i));
24 };
25
26 //...adding new tags
27 const handleAddition = (tag) => {
28 setTags([...tags, tag]);
29 };
30
31 //...runs when you click on a tag
32 const handleTagClick = (index) => {
33 console.log("The tag at index " + index + " was clicked");
34 };
35
36 return (
37 <div className='createPost'>
38 <form>
39 {/**...below the input fields---*/}
40 <ReactTags
41 tags={tags}
42 suggestions={suggestions}
43 delimiters={delimiters}
44 handleDelete={handleDelete}
45 handleAddition={handleAddition}
46 handleTagClick={handleTagClick}
47 inputFieldPosition='bottom'
48 autocomplete
49 />
50 <button className='createForm__button'>ADD POST</button>
51 </form>
52 </div>
53 );
54};
55
56export default CreatePost;
React Tags also allows us to customize its elements. Add the following code to the src/index.css
file:
1/*
2You can learn how it's styled here:
3https://stackblitz.com/edit/react-tag-input-1nelrc
4*/
5.ReactTags__tags react-tags-wrapper,
6.ReactTags__tagInput {
7 width: 100%;
8}
9.ReactTags__selected span.ReactTags__tag {
10 border: 1px solid #ddd;
11 background: #00abb3;
12 color: white;
13 font-size: 12px;
14 display: inline-block;
15 padding: 5px;
16 margin: 0 5px;
17 border-radius: 2px;
18 min-width: 100px;
19}
20
21.ReactTags__selected button.ReactTags__remove {
22 color: #fff;
23 margin-left: 15px;
24 cursor: pointer;
25 background-color: orangered;
26 padding: 0 10px;
27 border: none;
28 outline: none;
29}
30
31.ReactTags__tagInput input.ReactTags__tagInputField,
32.ReactTags__tagInput input.ReactTags__tagInputField:focus {
33 margin: 10px 0;
34 font-size: 12px;
35 width: 100%;
36 padding: 10px;
37 height: 45px;
38 text-transform: capitalize;
39 border: 1px solid #3c4048;
40}
41
42.ReactTags__selected span.ReactTags__tag {
43 border: 1px solid #ddd;
44 background: #63bcfd;
45 color: white;
46 font-size: 12px;
47 display: inline-block;
48 padding: 5px;
49 margin: 0 5px;
50 border-radius: 2px;
51}
52.ReactTags__selected a.ReactTags__remove {
53 color: #aaa;
54 margin-left: 5px;
55 cursor: pointer;
56}
57
58/* Styles for suggestions */
59.ReactTags__suggestions {
60 position: absolute;
61}
62.ReactTags__suggestions ul {
63 list-style-type: none;
64 box-shadow: 0.05em 0.01em 0.5em rgba(0, 0, 0, 0.2);
65 background: white;
66 width: 200px;
67}
68.ReactTags__suggestions li {
69 border-bottom: 1px solid #ddd;
70 padding: 5px 10px;
71 margin: 0;
72}
73.ReactTags__suggestions li mark {
74 text-decoration: underline;
75 background: none;
76 font-weight: 600;
77}
78.ReactTags__suggestions ul li.ReactTags__activeSuggestion {
79 background: #fff;
80 cursor: pointer;
81}
82
83.ReactTags__remove {
84 border: none;
85 cursor: pointer;
86 background: none;
87 color: white;
88}
Congratulations! We’ve completed the layout for the notion application. Next, let’s learn how to add all the needed functionalities with the Socket.io Node.js server.
Creating new posts with Socket.io
In this section, I’ll guide you on how to create new posts and display them on the React app with Socket.io.
Update the addPost
function within the CreatePost
component by sending the newly created post to the server via Socket.io.
1//👇🏻 Socket.io was passed from the App.js file
2const CreatePost = ({ socket }) => {
3 //...other functions
4
5 const addPost = (e) => {
6 e.preventDefault();
7
8 //👇🏻 sends all the post details to the server
9 socket.emit("createPost", {
10 postTitle,
11 postContent,
12 username: localStorage.getItem("username"),
13 timestamp: currentDate(),
14 tags,
15 });
16 navigate("/dashboard");
17 };
18
19 return <div className='createPost'>...</div>;
20};
Create a listener to the event on the server.
1socketIO.on("connection", (socket) => {
2 console.log(`⚡: ${socket.id} user just connected!`);
3
4 socket.on("createPost", (data) => {
5 /*👇🏻 data - contains all the post details
6 from the React app
7 */
8 console.log(data);
9 });
10
11 socket.on("disconnect", () => {
12 socket.disconnect();
13 console.log("🔥: A user disconnected");
14 });
15});
Create an array on the backend server that holds all the posts, and add the new post to the list.
1//👇🏻 generates a random ID
2const fetchID = () => Math.random().toString(36).substring(2, 10);
3
4let notionPosts = [];
5
6socket.on("createPost", (data) => {
7 const { postTitle, postContent, username, timestamp, tags } = data;
8 notionPosts.unshift({
9 id: fetchID(),
10 title: postTitle,
11 author: username,
12 createdAt: timestamp,
13 content: postContent,
14 comments: [],
15 });
16 //👉🏻 We'll use the tags later for sending notifications
17
18 //👇🏻 The notionposts are sent back to the React app via another event
19 socket.emit("updatePosts", notionPosts);
20});
Add a listener to the notion posts on the React app via the useEffect hook by copying the code below:
1//👇🏻 Within Home.js file
2
3useEffect(() => {
4 socket.on("updatePosts", (posts) => console.log(posts));
5}, [socket]);
Displaying the posts
Save the posts into a state and render them as below:
1import React, { useEffect, useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Home = ({ socket }) => {
5 const navigate = useNavigate();
6 const [posts, setPosts] = useState([]);
7
8 //👇🏻 Saves the posts into the "posts" state
9 useEffect(() => {
10 socket.on("updatePosts", (posts) => setPosts(posts));
11 }, [socket]);
12
13 const createPostBtn = () => navigate("/post/create");
14
15 //👇🏻 Navigates to the NotionPost page to view
16 // all the post contents
17 const readMoreBtn = (postID) => {
18 navigate(`/post/${postID}`);
19 };
20
21 return (
22 <div className='home'>
23 <nav className='home__navbar'>
24 <h2>HackNotion</h2>
25 <div className='home__buttons'>
26 <button className='home__createBtn' onClick={createPostBtn}>
27 CREATE POST
28 </button>
29 <button className='home__notifyBtn'>NOTIFY</button>
30 </div>
31 </nav>
32
33 <div className='posts__container'>
34 {posts?.map((post) => (
35 <div className='post' key={post.id}>
36 <h3>{post.title}</h3>
37 <button className='post__cta' onClick={() => readMoreBtn(post.id)}>
38 READ MORE
39 </button>
40 </div>
41 ))}
42 </div>
43 </div>
44 );
45};
46
47export default Home;
So far, we can only view the posts when we add one. Next, let’s make it possible for us to display the posts when we load the page.
Create a route on the server that returns the notion posts.
1app.get("/api", (req, res) => {
2 res.json(notionPosts);
3});
Update the Home.js
file fetch the notion posts and listen for new posts from the server.
1useEffect(() => {
2 function fetchPosts() {
3 fetch("http://localhost:4000/api")
4 .then((res) => res.json())
5 .then((data) => setPosts(data))
6 .catch((err) => console.error(err));
7 }
8 fetchPosts();
9}, []);
10
11useEffect(() => {
12 socket.on("updatePosts", (posts) => setPosts(posts));
13}, [socket]);
Completing the Notion Post component
In the previous section, you learnt how to create and display notion posts to users. Here, you’ll learn how to show the contents of each notion post when you click the Read More button.
Update the readMoreBtn
function within the Home.js
file as below:
1const readMoreBtn = (postID) => {
2 socket.emit("findPost", postID);
3//👇🏻 navigates to the Notionpost routenavigate(`/post/${postID}`);
4};
The code snippet above gets the ID of the selected post and sends a Socket.io event containing the post ID to the server before redirecting to the post route.
Create a listener to the findPost
event and return the post details via another Socket.io event.
1socket.on("findPost", (postID) => {
2
3 //👇🏻 Filter the notion post via the post ID
4 let result = notionPosts.filter((post) => post.id === postID);
5
6 //👇🏻 Returns a new event containing the post details
7 socket.emit("postDetails", result[0]);
8});
Listen to the postDetails
event with the NotionPost
component and render the post details as below:
1import React, { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4const NotionPost = ({ socket }) => {
5 //👇🏻 gets the Post ID from its URL
6 const { id } = useParams();
7
8 const [comment, setComment] = useState("");
9 const [post, setPost] = useState({});
10
11 //👇🏻loading state for async request
12 const [loading, setLoading] = useState(true);
13
14 //👇🏻 Gets the post details from the server for display
15 useEffect(() => {
16 socket.on("postDetails", (data) => {
17 setPost(data);
18 setLoading(false);
19 });
20 }, [socket]);
21
22 //👇🏻 Function for creating new comments
23 const handleAddComment = (e) => {
24 e.preventDefault();
25 console.log("newComment", {
26 comment,
27 user: localStorage.getItem("username"),
28 postID: id,
29 });
30 setComment("");
31 };
32
33 if (loading) {
34 return <h2>Loading... Please wait</h2>;
35 }
36
37 return (
38 <div className='notionPost'>
39 <div className='notionPost__container'>
40 <h1>{post.title}</h1>
41 <div className='notionPost__meta'>
42 <p className='notionPost__author'>By {post.author}</p>
43 <p className='notionPost__date'>Created on {post.createdAt}</p>
44 </div>
45
46 <div className='notionPost__content'>{post.content}</div>
47 </div>
48
49 <div className='comments__container'>
50 <h2>Add Comments</h2>
51 <form className='comments__inputContainer' onSubmit={handleAddComment}>
52 <textarea
53 placeholder='Type in your comments...'
54 rows={5}
55 className='comments__input'
56 value={comment}
57 required
58 onChange={(e) => setComment(e.target.value)}
59 />
60 <button className='comments__cta'>Add Comment</button>
61 </form>
62
63 <div>
64 <p className='comment'>
65 <span style={{ fontWeight: "bold" }}>Scopsy Dima</span> - Nice post
66 fam!❤️
67 </p>
68 </div>
69 </div>
70 </div>
71 );
72};
73
74export default NotionPost;
The Comments section
Here, I’ll guide you through adding comments to each notion post and displaying them in real-time.
Update the handleAddComment
function within the NotionPost
component to send the new comments details to the server.
1const handleAddComment = (e) => {
2 e.preventDefault();
3 socket.emit("newComment", {
4 comment,
5 user: localStorage.getItem("username"),
6 postID: id,
7 });
8 setComment("");
9};
Create a listener to the event on the server that adds the comment to the list of comments.
1socket.on("newComment", (data) => {
2 const { postID, user, comment } = data;
3
4//👇🏻 filters the notion post via its ID
5let result = notionPosts.filter((post) => post.id === postID);
6
7//👇🏻 Adds the comment to the comments list
8 result[0].comments.unshift({
9 id: fetchID(),
10 user,
11 message: comment,
12 });
13//👇🏻 sends the updated details to the React app
14 socket.emit("postDetails", result[0]);
15});
Update the NotionPost.js
file to display any existing comments.
1return (
2 <div className='notionPost'>
3 <div className='notionPost__container'>
4 <h1>{post.title}</h1>
5 <div className='notionPost__meta'>
6 <p className='notionPost__author'>By {post.author}</p>
7 <p className='notionPost__date'>Created on {post.createdAt}</p>
8 </div>
9
10 <div className='notionPost__content'>{post.content}</div>
11 </div>
12
13 <div className='comments__container'>
14 <h2>Add Comments</h2>
15 <form className='comments__inputContainer' onSubmit={handleAddComment}>
16 <textarea
17 placeholder='Type in your comments...'
18 rows={5}
19 className='comments__input'
20 value={comment}
21 required
22 onChange={(e) => setComment(e.target.value)}
23 />
24 <button className='comments__cta'>Add Comment</button>
25 </form>
26
27 {/** Displays existing comments to the user */}
28 <div>
29 {post.comments.map((item) => (
30 <p className='comment' key={item.id}>
31 <span style={{ fontWeight: "bold", marginRight: "15px" }}>
32 {item.user}
33 </span>
34 {item.message}
35 </p>
36 ))}
37 </div>
38 </div>
39 </div>
40);
Congratulations!🎊 You can now create posts and add comments with Socket.io. For the remaining part of this tutorial, I’ll guide you through sending notifications to every user you tag in your post using Novu.
How to add Novu to a React and Node.js application
Novu allows you to add various notification types, such as email, SMS, and in-app notifications. In this tutorial, you will learn how to create a Novu project, add Novu to your React and Node.js projects, and send an in-app notification with Novu.
Install the Novu Node.js SDK on the server and the Notification Center in the React app.
1👇🏻 Install on the client
2npm install @novu/notification-center
3
4👇🏻 Install on the server
5npm install @novu/node
Create a Novu project by running the code below. A personalised dashboard is available to you.
1👇🏻 Run on the client
2npx 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
1Now let's setup your account and send your first notification
2❓ What is your application name? Notionging-Platform
3❓ Now lets setup your environment. How would you like to proceed?
4 > Create a free cloud account (Recommended)
5❓ Create your account with:
6 > Sign-in with GitHub
7❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
8 > Yes
9✔️ Create your account successfully.
10
11We've created a demo web page for you to see novu notifications in action.
12Visit: 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.
Congratulations!🎊 You’ve successfully added Novu to your React and Node.js project. Next, let’s learn how to add in-app notifications to the notionging application to notify users when we tag them to a post.
Adding in-app notifications with Novu
Create a Notify.js
file within the src/components
folder and copy the code below into the file. It contains the elements required for in-app notifications 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 Notify = () => {
10 const navigate = useNavigate();
11
12 const onNotificationClick = (notification) =>
13 navigate(notification.cta.data.url);
14
15 return (
16 <div>
17 <NovuProvider
18 subscriberId='<YOUR_SUBSCRIBER_ID>'
19 applicationIdentifier='<YOUR_APP_ID>'
20 >
21 <PopoverNotificationCenter
22 onNotificationClick={onNotificationClick}
23 colorScheme='light'
24 >
25 {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
26 </PopoverNotificationCenter>
27 </NovuProvider>
28 </div>
29 );
30};
31
32export default Notify;
The code snippet above adds Novu notification bell icon to the Notify component, enabling us to view all the notifications within the application.
The
NovuProvider
component requires your Subscriber ID – copied earlier fromhttp://localhost:57807/demo
and your application ID available in the Settings section under API Keys on the Novu Manage Platform.
Import the Notify component into the Home.js
file and display the bell icon as below:
1return (
2 <div className='home'>
3 <nav className='home__navbar'>
4 <h2>HackNotion</h2>
5 <div className='home__buttons'>
6 <button className='home__createBtn' onClick={createPostBtn}>
7 CREATE POST
8 </button>
9 <Notify />
10 </div>
11 </nav>
12 </div>
13)
14
Next, create the workflow for the application, which describes the features you want to add to the application.
Select Notification from the Development sidebar and create a notification template. Click the newly created template, then, Workflow Editor, and ensure the workflow is as below
From the image above, Novu triggers the Digest engine before sending the in-app notification.
Novu Digest allows us to control how we want to send notifications within the application. It collects multiple trigger events and sends them as a single message. The image above sends notifications every 2 minutes, and it can be effective when you have many users and frequent updates.
Click the In-App
step from the image above and edit the notification template to contain the content below.
1{{sender}} tagged you to a post
Novu allows you to add dynamic content or data to the templates using the Handlebars templating engine. The data for the
sender
variable will be inserted into the template as a payload from the request within our app.
Save the template by clicking Update
button and head back to your code editor.
Sending notifications with Novu
Since we want to send notifications to users when we tag them to a post, we will have to store each username on the server, show them on the suggestion list provided by React Tags, and send them notifications via Novu.
Update the handleLogin
function within the Login.js
file to send the username to the server when they sign in.
1const handleLogin = (e) => {
2 e.preventDefault();
3 //👇🏻 sends the username to the server
4 socket.emit("addUser", username);
5 localStorage.setItem("username", username);
6 navigate("/dashboard");
7};
Listen to the event and store the username in an array on the server.
1let allUsers = [];
2
3socket.on("addUser", (user) => {
4 allUsers.push(user);
5});
Also, render the list of users via another route on the server.
1app.get("/users", (req, res) => {
2 res.json(allUsers);
3});
To show the users within the React Tags suggestion list, you need to send a request to the API route, get the list of users, and pass them into the list.
Update the CreatePost
function to fetch the list of users, and save them within the local storage before navigating the post/create
route.
1const createPostBtn = () => {
2 fetchUser();
3 navigate("/post/create");
4};
5
6const fetchUser = () => {
7 fetch("http://localhost:4000/users")
8 .then((res) => res.json())
9 .then((data) => {
10 //👇🏻 converts the array to a string
11 const stringData = data.toString();
12 //👇🏻 saved the data to local storage
13 localStorage.setItem("users", stringData);
14 })
15 .catch((err) => console.error(err));
16};
Next, retrieve all the users from the local storage and pass them into the suggestion list provided by React Tags for display within the CreatePost
component.
1const [users, setUsers] = useState([]);
2
3useEffect(() => {
4 function getUsers() {
5 const storedUsers = localStorage.getItem("users").split(",");
6 setUsers(storedUsers);
7 }
8 getUsers();
9}, []);
10
11const suggestions = users.map((name) => {
12 return {
13 id: name,
14 text: name,
15 };
16});
To notify each tagged user, create a function that loops through the users on the server and sends them a notification via Novu to them.
Import and initiate Novu on the server.
1const { Novu } = require("@novu/node");
2const novu = new Novu("<YOUR_API_KEY>")
Update the createPost
listener on the backend to send a notification to all tagged users.
1//👇🏻 Loops through the tagged users and sends a notification to each one of them
2const sendUsersNotification = (users, sender) => {
3 users.forEach(function (user) {
4 novuNotify(user, sender);
5 });
6};
7
8//👇🏻 sends a notification via Novu
9const novuNotify = async (user, sender) => {
10 try {
11 await novu
12 .trigger("<TEMPLATE_ID>", {
13 to: {
14 subscriberId: user.id,
15 firstName: user.text,
16 },
17 payload: {
18 sender: sender,
19 },
20 })
21 .then((res) => console.log("Response >>", res));
22 } catch (err) {
23 console.error("Error >>>>", { err });
24 }
25};
26
27socket.on("createPost", (data) => {
28 const { postTitle, postContent, username, timestamp, tags } = data;
29 notionPosts.unshift({
30 id: fetchID(),
31 title: postTitle,
32 author: username,
33 createdAt: timestamp,
34 content: postContent,
35 comments: [],
36 });
37 //👇🏻 Calls the function to send a notification to all tagged users
38 sendUsersNotification(tags, username);
39
40 socket.emit("updatePosts", notionPosts);
41});
Congratulations! 💃🏻 We’ve completed the code for this project.
Conclusion
So far, you’ve learned how to set up Socket.io in a React and Node.js application, send messages between the client and a Node.js server, add Novu to a React and Node.js application, and send notifications with Novu.
This tutorial demonstrates what you can build using Socket.io and Novu. Feel free to improve on the project by adding an authentication library and saving the blog posts to a database that supports real-time communication.
The complete code for this tutorial is available here: https://github.com/novuhq/blog/tree/main/blogging-platform-with-react-socketIO
Thank you for reading!