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ā¦
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.
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.
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.
mkdir notion-platformcd notion-platformmkdir client server
Navigate into the client folder via your terminal and create a new React.js project.
cd clientnpx 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.
npm 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.
Add the Socket.io client API to the React app as below:
import { io } from "socket.io-client";//\ud83d\udc47\ud83c\udffb http://localhost:4000 is where the server host URL.const socket = io.connect("http://localhost:4000");function App() { return ( Hello World!</p> </div> );}export default App;
Navigate into the server folder and create a package.json file.
cd server & npm init -y
Install Express.js, CORS, Nodemon, and Socket.io Server API.
npm 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.
touch 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.
Import the HTTP and the CORS library to allow data transfer between the client and the server domains.
const express = require("express");const app = express();const PORT = 4000;//\ud83d\udc47\ud83c\udffb New importsconst http = require("http").Server(app);const cors = require("cors");app.use(express.urlencoded({ extended: true }));app.use(express.json());app.use(cors());app.get("/api", (req, res) => { res.json({ message: "Hello world", });});http.listen(PORT, () => { console.log(`Server listening on ${PORT}`);});
Next, add Socket.io to the project to create a real-time connection. Before theĀ app.get()Ā block, copy the code below.
//\ud83d\udc47\ud83c\udffb New imports.....const socketIO = require('socket.io')(http, { cors: { origin: "http://localhost:3000" }});//\ud83d\udc47\ud83c\udffb Add this before the app.get() blocksocketIO.on('connection', (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on('disconnect', () => { socket.disconnect() console.log('\ud83d\udd25: A user disconnected'); });});
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.
//\ud83d\udc47\ud83c\udffb In server/package.json"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon index.js" }
You can now run the server with Nodemon by using the command below.
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:
import React, { useState } from "react";const NotionPost = () => { const [comment, setComment] = useState(""); const handleAddComment = (e) => { e.preventDefault(); console.log({ comment }); setComment(""); }; return ( How to create a new React Native project with Expo</h1> By Nevo David</p> Created on 22nd September, 2022</p> </div> For this article, I will use Puppeteer and ReactJS. Puppeteer is a Node.js library that automates several browser actions such as form submission. </div> </div> Add Comments</h2> setComment(e.target.value)} /> Add Comment</button> </form> Scopsy Dima</span> - Nice post fam!\u2764\ufe0f </p> </div> </div> </div> );};export default NotionPost;
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.
import React, { useState } from "react";import { useNavigate } from "react-router-dom";const CreatePost = () => { const navigate = useNavigate(); const [postTitle, setPostTitle] = useState(""); const [postContent, setPostContent] = useState(""); //...gets the publish date for the post const currentDate = () => { const d = new Date(); return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}`; }; //...logs the post details to the console const addPost = (e) => { e.preventDefault(); console.log({ postTitle, postContent, username: localStorage.getItem("username"), timestamp: currentDate(), }); navigate("/dashboard"); }; return ( <> Create a new Post</h2> Title</label> setPostTitle(e.target.value)} className='createForm__title' /> Content</label> setPostContent(e.target.value)} className='createForm__content' /> ADD POST</button> </form> </div> </> );};export default CreatePost;
Import React Tags into theĀ CreatePost.jsĀ file:
import { WithContext as ReactTags } from "react-tag-input";
Update the CreatePost component to contain the code snippet below for creating tags with React Tags.
//\ud83d\udc47\ud83c\udffb The suggestion list for autocompleteconst suggestions = ["Tomer", "David", "Nevo"].map((name) => { return { id: name, text: name, };});const KeyCodes = { comma: 188, enter: 13,};//\ud83d\udc47\ud83c\udffb The comma and enter keys are used to separate each tagsconst delimiters = [KeyCodes.comma, KeyCodes.enter];//...The React componentconst CreatePost = () => { //\ud83d\udc47\ud83c\udffb An array containing the tags const [tags, setTags] = useState([]); //...deleting tags const handleDelete = (i) => { setTags(tags.filter((tag, index) => index !== i)); }; //...adding new tags const handleAddition = (tag) => { setTags([...tags, tag]); }; //...runs when you click on a tag const handleTagClick = (index) => { console.log("The tag at index " + index + " was clicked"); }; return ( {/**...below the input fields---*/} ADD POST</button> </form> </div> );};export default CreatePost;
React Tags also allows us to customize its elements. Add the following code to theĀ src/index.cssĀ file:
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.
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.
//\ud83d\udc47\ud83c\udffb Socket.io was passed from the App.js fileconst CreatePost = ({ socket }) => { //...other functions const addPost = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb sends all the post details to the server socket.emit("createPost", { postTitle, postContent, username: localStorage.getItem("username"), timestamp: currentDate(), tags, }); navigate("/dashboard"); }; return ...</div>;};
Create a listener to the event on the server.
socketIO.on("connection", (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on("createPost", (data) => { /*\ud83d\udc47\ud83c\udffb data - contains all the post details from the React app */ console.log(data); }); socket.on("disconnect", () => { socket.disconnect(); console.log("\ud83d\udd25: A user disconnected"); });});
Create an array on the backend server that holds all the posts, and add the new post to the list.
//\ud83d\udc47\ud83c\udffb generates a random IDconst fetchID = () => Math.random().toString(36).substring(2, 10);let notionPosts = [];socket.on("createPost", (data) => { const { postTitle, postContent, username, timestamp, tags } = data; notionPosts.unshift({ id: fetchID(), title: postTitle, author: username, createdAt: timestamp, content: postContent, comments: [], }); //\ud83d\udc49\ud83c\udffb We'll use the tags later for sending notifications //\ud83d\udc47\ud83c\udffb The notionposts are sent back to the React app via another event socket.emit("updatePosts", notionPosts);});
Add a listener to the notion posts on the React app via the useEffect hook by copying the code below:
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:
const readMoreBtn = (postID) => { socket.emit("findPost", postID);//\ud83d\udc47\ud83c\udffb navigates to the Notionpost routenavigate(`/post/${postID}`);};
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.
socket.on("findPost", (postID) => { //\ud83d\udc47\ud83c\udffb Filter the notion post via the post ID let result = notionPosts.filter((post) => post.id === postID); //\ud83d\udc47\ud83c\udffb Returns a new event containing the post details socket.emit("postDetails", result[0]);});
Listen to theĀ postDetailsĀ event with theĀ NotionPostĀ component and render the post details as below:
import React, { useState, useEffect } from "react";import { useParams } from "react-router-dom";const NotionPost = ({ socket }) => { //\ud83d\udc47\ud83c\udffb gets the Post ID from its URL const { id } = useParams(); const [comment, setComment] = useState(""); const [post, setPost] = useState({}); //\ud83d\udc47\ud83c\udffbloading state for async request const [loading, setLoading] = useState(true); //\ud83d\udc47\ud83c\udffb Gets the post details from the server for display useEffect(() => { socket.on("postDetails", (data) => { setPost(data); setLoading(false); }); }, [socket]); //\ud83d\udc47\ud83c\udffb Function for creating new comments const handleAddComment = (e) => { e.preventDefault(); console.log("newComment", { comment, user: localStorage.getItem("username"), postID: id, }); setComment(""); }; if (loading) { return Loading... Please wait</h2>; } return ( {post.title}</h1> By {post.author}</p> Created on {post.createdAt}</p> </div> {post.content}</div> </div> Add Comments</h2> setComment(e.target.value)} /> Add Comment</button> </form> Scopsy Dima</span> - Nice post fam!\u2764\ufe0f </p> </div> </div> </div> );};export default NotionPost;
Create a listener to the event on the server that adds the comment to the list of comments.
socket.on("newComment", (data) => { const { postID, user, comment } = data;//\ud83d\udc47\ud83c\udffb filters the notion post via its IDlet result = notionPosts.filter((post) => post.id === postID);//\ud83d\udc47\ud83c\udffb Adds the comment to the comments list result[0].comments.unshift({ id: fetchID(), user, message: comment, });//\ud83d\udc47\ud83c\udffb sends the updated details to the React app socket.emit("postDetails", result[0]);});
Update theĀ NotionPost.jsĀ file to display any existing comments.
return ( {post.title}</h1> By {post.author}</p> Created on {post.createdAt}</p> </div> {post.content}</div> </div> Add Comments</h2> setComment(e.target.value)} /> Add Comment</button> </form> {/** Displays existing comments to the user */} {post.comments.map((item) => ( {item.user} </span> {item.message} </p> ))} </div> </div> </div>);
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.
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.
\ud83d\udc47\ud83c\udffb Install on the clientnpm install @novu/notification-center\ud83d\udc47\ud83c\udffb Install on the servernpm install @novu/node
Create a Novu project by running the code below. A personalised dashboard is available to you.
\ud83d\udc47\ud83c\udffb Run on the clientnpx 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\u2753 What is your application name? Notionging-Platform\u2753 Now lets setup your environment. How would you like to proceed? > Create a free cloud account (Recommended)\u2753 Create your account with: > Sign-in with GitHub\u2753 I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy) > Yes\u2714\ufe0f 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.
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.
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.
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 from Ā http://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:
return ( HackNotion</h2> CREATE POST </button> </div> </nav> </div>)\ufeff
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-Appstep from the image above and edit the notification template to contain the content below.
{{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.
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.
const handleLogin = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb sends the username to the server socket.emit("addUser", username); localStorage.setItem("username", username); navigate("/dashboard");};
Listen to the event and store the username in an array on the server.
let allUsers = [];socket.on("addUser", (user) => { allUsers.push(user);});
Also, render the list of users via another route on the server.
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.
const createPostBtn = () => { fetchUser(); navigate("/post/create");};const fetchUser = () => { fetch("http://localhost:4000/users") .then((res) => res.json()) .then((data) => { //\ud83d\udc47\ud83c\udffb converts the array to a string const stringData = data.toString(); //\ud83d\udc47\ud83c\udffb saved the data to local storage localStorage.setItem("users", stringData); }) .catch((err) => console.error(err));};
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.
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.