Building THE MOST practical Todolist with React and Websockets 🪄✨
In this article, you'll learn how to build a team to-do list with React.js and Socket.io. Users can create, read, and delete to-dos and add comments to each to-do via Socket.io. You'll also learn how to add notifications to the application when you…
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.
I would be super happy if you could give us a star! It will help me to make more articles every week 🚀 https://github.com/novuhq/novu
Socket.io is a popular JavaScript library that allows us to create real-time, bi-directional communication between web browsers and a Node.js server. It is a highly performant and reliable library designed to process a large volume of data with minimal delay. It follows the WebSocket protocol and provides better functionalities, such as fallback to HTTP long-polling or automatic reconnection, which enables us to build efficient real-time applications.
Here, we’ll set up the project environment for the to-do list application. 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 todo-listcd todo-listmkdir 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.
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.
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.
npm install express cors nodemon socket.io
Create an index.js file – the entry point to the web server.
touch 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.
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;app.use(express.urlencoded({ extended: true }));app.use(express.json());//\ud83d\udc47\ud83c\udffb New importsconst http = require("http").Server(app);const cors = require("cors");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.
Here, we’ll create the user interface for the to-do list application. Users will be able to sign in, add and delete a to-do, and add comments to every to-do.
Navigate into the client/src folder and create a components folder containing a Home.js and Main.js file.
cd clientmkdir componentscd componentstouch Home.js Main.js
Update the App.js file to render the newly created components on different routes via React Router.
import React from "react";import socketIO from "socket.io-client";import { BrowserRouter, Routes, Route } from "react-router-dom";import Main from "./components/Main";import Home from "./components/Home";const socket = socketIO.connect("http://localhost:4000");const App = () => { return ( } /> } /> </Routes> </BrowserRouter> );};export default App;
Navigate into the src/index.css file and copy the code below. It contains all the CSS required for styling this project.
Here, we’ll build the user interface for the central part of the application.
Copy the code snippet below into the Main.js file.
import React, { useState } from "react";import Nav from "./Nav";function Main({ socket }) { const [todo, setTodo] = useState(""); //\ud83d\udc47\ud83c\udffb Generates a random string as the todo ID const generateID = () => Math.random().toString(36).substring(2, 10); const handleAddTodo = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb Every todo has this structure - id, todo & comments. console.log({ id: generateID(), todo, comments: [], }); setTodo(""); }; return ( setTodo(e.target.value)} className='input' required /> ADD TODO</button> </form> Contributing to open-source</p> View Comments</button> DELETE</button> </div> </div> Coffee chat with the team</p> View Comments</button> DELETE</button> </div> </div> Work on my side projects</p> View Comments</button> DELETE</button> </div> </div> </div> </div> );}export default Main;
The code snippet above represents the user interface that enables users to create a to-do, view comments, and delete existing to-dos.
The Nav component is the navigation bar for the application – later in this tutorial, we’ll send notifications with Novu within this component.
Create the Nav component and copy the code below into it:
//\ud83d\udc47\ud83c\udffb Within /src/components/Nav.jsimport React from "react";const Nav = () => { return ( Todo List</h2> </nav> );};export default Nav;
Congratulations!🔥 We’ve created the user interface for the application. In the upcoming sections, you’ll learn how to send real-time data with Socket.io and notifications with Novu.
In this section, I’ll guide you through creating new to-dos and display them on the React app with Socket.io.
Update the handleAddTodo function within the Main.js file to send the new to-do to the server via Socket.io.
const handleAddTodo = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb Sends a event - addTodo via Socket.io // containing the id, todo, and the comments array socket.emit("addTodo", { id: generateID(), todo, comments: [], }); setTodo("");};
Create a listener to the event on the server.
socketIO.on("connection", (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on("addTodo", (todo) => { //\ud83d\udc47\ud83c\udffb todo - contains the object from the React app console.log(todo); }); socket.on("disconnect", () => { socket.disconnect(); console.log("\ud83d\udd25: A user disconnected"); });});
Create an array on the backend server that holds all the to-dos, and add the new to-do to the list.
//\ud83d\udc47\ud83c\udffb Array containing all the to-doslet todoList = [];socketIO.on("connection", (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on("addTodo", (todo) => { //\ud83d\udc47\ud83c\udffb Adds the to-do object to the list of to-dos todoList.unshift(todo); //\ud83d\udc47\ud83c\udffb Sends all the to-dos to the React app socket.emit("todos", todoList); }); socket.on("disconnect", () => { socket.disconnect(); console.log("\ud83d\udd25: A user disconnected"); });});
Create a listener for the to-dos on the React app via the useEffect hook. Copy the code below:
Create a listener for the deleteTodo event that removes the to-do via its ID from the to-do list.
//\ud83d\udc47\ud83c\udffb Array containing all the to-doslet todoList = [];socketIO.on("connection", (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on("addTodo", (todo) => { todoList.unshift(todo); socket.emit("todos", todoList); }); //\ud83d\udc47\ud83c\udffb Filters the array of to-dos and // sends the updated to-do to the React app. socket.on("deleteTodo", (id) => { todoList = todoList.filter((todo) => todo.id !== id); //\ud83d\udc47\ud83c\udffb Sends the updated to-do to the React app socket.emit("todos", todoList); }); socket.on("disconnect", () => { socket.disconnect(); console.log("\ud83d\udd25: A user disconnected"); });});
You can now add and delete each to-do via Socket.io. Next, you’ll learn how to add and display comments for each to-do.
To make this component display as a Modal, we need to give it some styling as done below within the src/index.css file, especially the position and z-index property.
Next, let’s toggle the Modal.js component when we click on the View Comment button within the Main.js file.
import React, { useState, useEffect } from "react";import Nav from "./Nav";import Modal from "./Modal";function Main({ socket }) { const [todo, setTodo] = useState(""); const [todoList, setTodoList] = useState([]); const [showModal, setShowModal] = useState(false); const toggleModal = () => setShowModal(!showModal); //...other functions return ( ... {todoList.map((item) => ( {item.todo}</p> {/*\ud83d\udc47\ud83c\udffb This button toggles the Modal component---*/} View Comments </button> deleteTodo(item.id)}> DELETE </button> </div> </div> ))} </div> {/*\ud83d\udc47\ud83c\udffb The Modal replaces the Main component*/} {showModal ? ( ) : ( "" )} </div> );}export default Main;
Since we’ve been able to display the Modal when we click on the View Button, next, let’s toggle the Modal when we click outside the comments container.
Update the Modal.js file as below:
import React, { useState, useRef } from "react";const Modal = ({ socket, showModal, setShowModal }) => { const [comment, setComment] = useState(""); const modalRef = useRef(); //\ud83d\udc47\ud83c\udffb If the container (modalRef) is clicked, it closes the modal. const closeModal = (e) => { if (modalRef.current === e.target) { setShowModal(!showModal); } }; const addComment = (e) => { e.preventDefault(); console.log({ comment }); setComment(""); }; return ( ... </div> );};
Congratulations!💃🏻 You’ve learnt how to add Modals to a React application. Hence, let’s make it possible to users to add and display comments.
Create a listener on the server that accepts the to-do ID, fetches its details, and sends it back to the React app.
socket.on("viewComments", (id) => { for (let i = 0; i < todoList.length; i++) { if (id === todoList[i].id) { //\ud83d\udc47\ud83c\udffb sends the todo details back to the React app for display socket.emit("commentsReceived", todoList[i]); } }});
Create a listener for the commentsReceived event within the Modal.js file.
Create a state within the Main.js that holds the ID of the selected to-do. Pass the state to the Modal.js component.
const toggleModal = (itemId) => { socket.emit("viewComments", itemId); //\ud83d\udc47\ud83c\udffb Pass this ID into the Modal component setSelectedItemID(itemId); setShowModal(!showModal);};
Update the addComment function within the Modal.js file to send the comment details to the server.
const addComment = (e) => { e.preventDefault(); socket.emit("updateComment", { todoID: selectedItemID, //The ID passed from the Main.js file comment, user: localStorage.getItem("_username"), }); setComment("");};
Create a listener for the addComment event on the server that adds the comment to the to-do’s comments.
socket.on("updateComment", (data) => { //\ud83d\udc47\ud83c\udffb Destructure the items from the object const { user, todoID, comment } = data; for (let i = 0; i < todoList.length; i++) { //\ud83d\udc47\ud83c\udffb Gets the todo if (todoID === todoList[i].id) { //\ud83d\udc47\ud83c\udffb Add the comment to the list of comments todoList[i].comments.push({ name: user, text: comment }); //\ud83d\udc47\ud83c\udffb Sends an update to React app socket.emit("commentsReceived", todoList[i]); } }});
Congratulations! We can now add comments to each to-do and display them on the React app.
If you want to add notifications to the application when a user adds a comment or a new to-do, you can do that easily with Novu within the Nav.js component.
Novu allows you to add various notification types, such as email, SMS, and in-app notifications.
To add the in-app notification, 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 personalized dashboard is available to you.
\ud83d\udc47\ud83c\udffb Install 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? Devto Clone\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.
Update the components/Nav.jsfile to contain Novu and its required elements for in-app notifications from the documentation.
import React from "react";import { NovuProvider, PopoverNotificationCenter, NotificationBell,} from "@novu/notification-center";import { useNavigate } from "react-router-dom";const Nav = () => { const navigate = useNavigate(); const onNotificationClick = (notification) => navigate(notification.cta.data.url); return ( Todo List</h2> {({ unseenCount }) => ( )} </PopoverNotificationCenter> </NovuProvider> </div> </nav> );};export default Nav;
The code snippet above adds Novu notification bell icon to the Nav component, enabling us to view all the notifications from the application.
💡 The NovuProvider component requires your Subscriber ID – copied earlier from http://localhost:57807/demoand your application ID available in the Settings section under API Keys on the Novu Manage Platform.
Next, let’s 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. Select the newly created template, click on 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 and edit the notification template to contain the content below.
{{userId}} added a new to-do.
💡 💡 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 inserted into the template as a payload from the request.
Save the template by clicking Update button and head back to your code editor.
So far, you’ve learnt how to set up Socket.io in a React and Node.js application, and communicate between a server and a client via Socket.io.
This is a demo of what you can build using Socket.io and React. Feel free to improve the application by adding authentication, a real-time database, and notifications via Novu when a user drops a comment.The source code for this tutorial is available here:https://github.com/novuhq/blog/tree/main/todolist-with-react-and-socketIO
P.S Novu is sending awesome swag on Hacktoberfest! Come and participate! Happy if you can support us by giving us a star! ⭐️