category: How toSep 28, 2022

Building a beautiful Kanban board with Node.js, React, and Websockets 🦄 ✨

In this article, you'll learn how to build a Kanban Board the same as you have in JIRA, Monday and Trello. We will do it with a beautiful drag-and-drop feature using React, Socket.io, and React beautiful DND. Users will be able to sign in, create and update various tasks, and add comments.

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.

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

Novu
We will also send some awesome swag during Hacktoberfest 😇

What is Socket.io?

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 optimized 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.

isthereajira

How to create a real-time connection with Socket.io & React.js

Here, we’ll set up the project environment for the project. 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 todo-list
2cd todo-list
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.

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            socket.disconnect()
14      console.log('🔥: A user disconnected');
15    });
16});

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

Building the user interface

Here, we’ll create the user interface for the application. It is divided into three pages: the Login page, Task page – the central part of the application, and The Comments page – where users can comment on each task.

Navigate into client/src and create a components folder containing the Login.jsTask.js, and Comments.js files.

1cd client/src
2mkdir components
3cd components
4touch Login.js Task.js Comments.js

Update the App.js file to render the newly created components on different routes via React Router.

1import { BrowserRouter, Route, Routes } from "react-router-dom";
2import Comments from "./components/Comments";
3import Task from "./components/Task";
4import Login from "./components/Login";
5
6function App() {
7    return (
8        <BrowserRouter>
9            <Routes>
10                <Route path='/' element={<Login />} />
11                <Route path='/task' element={<Task />} />
12                <Route path='/comments/:category/:id' element={<Comments />} />
13            </Routes>
14        </BrowserRouter>
15    );
16}
17
18export default App;

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    font-family: "Space Grotesk", sans-serif;
4    box-sizing: border-box;
5}
6a {
7    text-decoration: none;
8}
9body {
10    margin: 0;
11    padding: 0;
12}
13.navbar {
14    width: 100%;
15    background-color: #f1f7ee;
16    height: 10vh;
17    border-bottom: 1px solid #ddd;
18    display: flex;
19    align-items: center;
20    justify-content: space-between;
21    padding: 20px;
22}
23.form__input {
24    min-height: 20vh;
25    display: flex;
26    align-items: center;
27    justify-content: center;
28}
29.input {
30    margin: 0 5px;
31    width: 50%;
32    padding: 10px 15px;
33}
34.addTodoBtn {
35    width: 150px;
36    padding: 10px;
37    cursor: pointer;
38    background-color: #367e18;
39    color: #fff;
40    border: none;
41    outline: none;
42    height: 43px;
43}
44.container {
45    width: 100%;
46    min-height: 100%;
47    display: flex;
48    align-items: center;
49    justify-content: space-between;
50    padding: 10px;
51}
52
53.completed__wrapper,
54.ongoing__wrapper,
55.pending__wrapper {
56    width: 32%;
57    min-height: 60vh;
58    display: flex;
59    flex-direction: column;
60    padding: 5px;
61}
62.ongoing__wrapper > h3,
63.pending__wrapper > h3,
64.completed__wrapper > h3 {
65    text-align: center;
66    text-transform: capitalize;
67}
68.pending__items {
69    background-color: #eee3cb;
70}
71.ongoing__items {
72    background-color: #d2daff;
73}
74.completed__items {
75    background-color: #7fb77e;
76}
77.pending__container,
78.ongoing__container,
79.completed__container {
80    width: 100%;
81    min-height: 55vh;
82    display: flex;
83    flex-direction: column;
84    padding: 5px;
85    border: 1px solid #ddd;
86    border-radius: 5px;
87}
88.pending__items,
89.ongoing__items,
90.completed__items {
91    width: 100%;
92    border-radius: 5px;
93    margin-bottom: 10px;
94    padding: 15px;
95}
96.comment {
97    text-align: right;
98    font-size: 14px;
99    cursor: pointer;
100    color: rgb(85, 85, 199);
101}
102.comment:hover {
103    text-decoration: underline;
104}
105.comments__container {
106    padding: 20px;
107}
108.comment__form {
109    width: 100%;
110    display: flex;
111    align-items: center;
112    justify-content: center;
113    flex-direction: column;
114    margin-bottom: 30px;
115}
116.comment__form > label {
117    margin-bottom: 15px;
118}
119.comment__form textarea {
120    width: 80%;
121    padding: 15px;
122    margin-bottom: 15px;
123}
124.commentBtn {
125    padding: 10px;
126    width: 200px;
127    background-color: #367e18;
128    outline: none;
129    border: none;
130    color: #fff;
131    height: 45px;
132    cursor: pointer;
133}
134.comments__section {
135    width: 100%;
136    display: flex;
137    align-items: center;
138    justify-content: center;
139    flex-direction: column;
140}
141
142.login__form {
143    width: 100%;
144    height: 100vh;
145    display: flex;
146    flex-direction: column;
147    align-items: center;
148    justify-content: center;
149}
150.login__form > label {
151    margin-bottom: 15px;
152}
153.login__form > input {
154    width: 70%;
155    padding: 10px 15px;
156    margin-bottom: 15px;
157}
158.login__form > button {
159    background-color: #367e18;
160    color: #fff;
161    padding: 15px;
162    cursor: pointer;
163    border: none;
164    font-size: 16px;
165    outline: none;
166    width: 200px;
167}

The Login page

Here, the application accepts the username and saves it in the local storage for identification.

Update the Login.js file as below:

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        //👇🏻 saves the username to localstorage
11        localStorage.setItem("userId", username);
12        setUsername("");
13        //👇🏻 redirects to the Tasks page.
14        navigate("/tasks");
15    };
16    return (
17        <div className='login__container'>
18            <form className='login__form' onSubmit={handleLogin}>
19                <label htmlFor='username'>Provide a username</label>
20                <input
21                    type='text'
22                    name='username'
23                    id='username'
24                    required
25                    onChange={(e) => setUsername(e.target.value)}
26                    value={username}
27                />
28                <button>SIGN IN</button>
29            </form>
30        </div>
31    );
32};
33
34export default Login;

The Task page

Here, I’ll guide you through creating the web layout for the Tasks page. The image below represents the page’s layout.

Task Page

Divide the layout into three components namely: Nav.jsAddTask.js – the form input section, and TasksContainer.js – containing the tasks.

1cd src/components
2touch Nav.js AddTask.js TasksContainer.js

Render the components within the Task.js file.

1import React from "react";
2import AddTask from "./AddTask";
3import TasksContainer from "./TasksContainer";
4import Nav from "./Nav";
5import socketIO from "socket.io-client";
6
7/*
8👇🏻  Pass Socket.io into the required components
9    where communications are made with the server
10*/
11const socket = socketIO.connect("http://localhost:4000");
12
13const Task = () => {
14    return (
15        <div>
16            <Nav />
17            <AddTask socket={socket} />
18            <TasksContainer socket={socket} />
19        </div>
20    );
21};
22
23export default Task;

Copy the code below into the Nav.js file.

1import React from "react";
2
3const Nav = () => {
4    return (
5        <nav className='navbar'>
6            <h3>Team's todo list</h3>
7        </nav>
8    );
9};
10export default Nav;

Update the AddTask.js file as below:

1import React, { useState } from "react";
2
3const AddTask = ({ socket }) => {
4    const [task, setTask] = useState("");
5
6    const handleAddTodo = (e) => {
7        e.preventDefault();
8        //👇🏻 Logs the task to the console
9        console.log({ task });
10        setTask("");
11    };
12    return (
13        <form className='form__input' onSubmit={handleAddTodo}>
14            <label htmlFor='task'>Add Todo</label>
15            <input
16                type='text'
17                name='task'
18                id='task'
19                value={task}
20                className='input'
21                required
22                onChange={(e) => setTask(e.target.value)}
23            />
24            <button className='addTodoBtn'>ADD TODO</button>
25        </form>
26    );
27};
28
29export default AddTask;

Copy the code below into the TasksContainer.js file. It renders three parent elements for the pending, ongoing, and completed tasks.

1import React from "react";
2import { Link } from "react-router-dom";
3
4const TasksContainer = ({ socket }) => {
5    return (
6        <div className='container'>
7            <div className='pending__wrapper'>
8                <h3>Pending Tasks</h3>
9                <div className='pending__container'>
10                    <div className='pending__items'>
11                        <p>Debug the Notification center</p>
12                        <p className='comment'>
13                            <Link to='/comments'>2 Comments</Link>
14                        </p>
15                    </div>
16                </div>
17            </div>
18
19            <div className='ongoing__wrapper'>
20                <h3>Ongoing Tasks</h3>
21                <div className='ongoing__container'>
22                    <div className='ongoing__items'>
23                        <p>Create designs for Novu</p>
24                        <p className='comment'>
25                            <Link to='/comments'>Add Comment</Link>
26                        </p>
27                    </div>
28                </div>
29            </div>
30
31            <div className='completed__wrapper'>
32                <h3>Completed Tasks</h3>
33                <div className='completed__container'>
34                    <div className='completed__items'>
35                        <p>Debug the Notification center</p>
36                        <p className='comment'>
37                            <Link to='/comments'>2 Comments</Link>
38                        </p>
39                    </div>
40                </div>
41            </div>
42        </div>
43    );
44};
45
46export default TasksContainer;

Congratulations!💃🏻 The layout is now set. Hence, let’s create a simple template for the Comments page.

The Comments page

Copy the code below into the Comments.js file. It logs the comment and the username to the console.

1import React, { useEffect, useState } from "react";
2import socketIO from "socket.io-client";
3import { useParams } from "react-router-dom";
4
5const socket = socketIO.connect("http://localhost:4000");
6
7const Comments = () => {
8    const [comment, setComment] = useState("");
9
10    const addComment = (e) => {
11        e.preventDefault();
12        console.log({
13            comment,
14            userId: localStorage.getItem("userId"),
15        });
16        setComment("");
17    };
18
19    return (
20        <div className='comments__container'>
21            <form className='comment__form' onSubmit={addComment}>
22                <label htmlFor='comment'>Add a comment</label>
23                <textarea
24                    placeholder='Type your comment...'
25                    value={comment}
26                    onChange={(e) => setComment(e.target.value)}
27                    rows={5}
28                    id='comment'
29                    name='comment'
30                    required
31                ></textarea>
32                <button className='commentBtn'>ADD COMMENT</button>
33            </form>
34
35            <div className='comments__section'>
36                <h2>Existing Comments</h2>
37                <div></div>
38            </div>
39        </div>
40    );
41};
42
43export default Comments;

The user interface is now complete. Next, let’s add React Beautiful DND to the application to enable the drag-and-drop feature.

How to add the drag-and-drop feature with React Beautiful DND

Here, you’ll learn how to add the drag-and-drop feature using React Beautiful DND and communicate between the React app and a Socket.io Node.js server.

How does React Beautiful DND work?

React Beautiful DND is a highly performant library that allows us to select and drag an item from its current position to another position on the page.

https://user-images.githubusercontent.com/2182637/53607406-c8f3a780-3c12-11e9-979c-7f3b5bd1bfbd.gif

The image above explains how to set up React Beautiful DND. You must wrap all the draggable and droppable items within the <DragDropContext/>. The <Droppable/> component holds the draggable items placed within the <Draggable/> component.

Making the tasks draggable and droppable with React Beautiful DND

Here, you’ll learn how to add React Beautiful DND to the React app and make the tasks movable from one category to another (pending, ongoing, and completed).

Install React Beautiful DND and ensure you are not using React in strict mode. (Check src/index.js).

1npm install react-beautiful-dnd

Open the server/index.js file and create an object containing all the dummy data for each task category.

1//👇🏻 server/index.js
2
3//👇🏻 Generates a random string
4const fetchID = () => Math.random().toString(36).substring(2, 10);
5
6//👇🏻 Nested object
7let tasks = {
8    pending: {
9        title: "pending",
10        items: [
11            {
12                id: fetchID(),
13                title: "Send the Figma file to Dima",
14                comments: [],
15            },
16        ],
17    },
18    ongoing: {
19        title: "ongoing",
20        items: [
21            {
22                id: fetchID(),
23                title: "Review GitHub issues",
24                comments: [
25                    {
26                        name: "David",
27                        text: "Ensure you review before merging",
28                        id: fetchID(),
29                    },
30                ],
31            },
32        ],
33    },
34    completed: {
35        title: "completed",
36        items: [
37            {
38                id: fetchID(),
39                title: "Create technical contents",
40                comments: [
41                    {
42                        name: "Dima",
43                        text: "Make sure you check the requirements",
44                        id: fetchID(),
45                    },
46                ],
47            },
48        ],
49    },
50};
51
52//👇🏻 host the tasks object via the /api route
53app.get("/api", (req, res) => {
54    res.json(tasks);
55});

Next, fetch the tasks within the TasksContainer.js file. The code snippet below converts the tasks object to an array before rendering the component.

1import React, { useState, useEffect } from "react";
2import { Link } from "react-router-dom";
3
4const TasksContainer = () => {
5    const [tasks, setTasks] = useState({});
6
7    useEffect(() => {
8        function fetchTasks() {
9            fetch("http://localhost:4000/api")
10                .then((res) => res.json())
11                .then((data) => {
12                    console.log(data);
13                    setTasks(data);
14                });
15        }
16        fetchTasks();
17    }, []);
18
19    return (
20        <div className='container'>
21            {/* 
22            👇🏻 Returns an array of each tasks (Uncomment to view the data structure)
23
24             {Object.entries(tasks).map((task) => console.log(task))} */}
25
26            {Object.entries(tasks).map((task) => (
27                <div
28                    className={`${task[1].title.toLowerCase()}__wrapper`}
29                    key={task[1].title}
30                >
31                    <h3>{task[1].title} Tasks</h3>
32                    <div className={`${task[1].title.toLowerCase()}__container`}>
33                        {task[1].items.map((item, index) => (
34                            <div
35                                className={`${task[1].title.toLowerCase()}__items`}
36                                key={item.id}
37                            >
38                                <p>{item.title}</p>
39                                <p className='comment'>
40                                    <Link to='/comments'>
41                                        {item.comments.length > 0 ? `View Comments` : "Add Comment"}
42                                    </Link>
43                                </p>
44                            </div>
45                        ))}
46                    </div>
47                </div>
48            ))}
49        </div>
50    );
51};
52
53export default TasksContainer;

Import the required components from “react-beautiful-dnd” into the TasksContainer.js file.

1//👇🏻 At the top of the TasksContainer.js file
2import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

Update the TaskContainer.js file as below:

1return (
2    <div className='container'>
3        {/** --- 👇🏻 DragDropContext  ---- */}
4        <DragDropContext onDragEnd={handleDragEnd}>
5            {Object.entries(tasks).map((task) => (
6                <div
7                    className={`${task[1].title.toLowerCase()}__wrapper`}
8                    key={task[1].title}
9                >
10                    <h3>{task[1].title} Tasks</h3>
11                    <div className={`${task[1].title.toLowerCase()}__container`}>
12                        {/** --- 👇🏻 Droppable --- */}
13                        <Droppable droppableId={task[1].title}>
14                            {(provided) => (
15                                <div ref={provided.innerRef} {...provided.droppableProps}>
16                                    {task[1].items.map((item, index) => (
17                                            {/** --- 👇🏻 Draggable --- */}
18                                        <Draggable
19                                            key={item.id}
20                                            draggableId={item.id}
21                                            index={index}
22                                        >
23                                            {(provided) => (
24                                                <div
25                                                    ref={provided.innerRef}
26                                                    {...provided.draggableProps}
27                                                    {...provided.dragHandleProps}
28                                                    className={`${task[1].title.toLowerCase()}__items`}
29                                                >
30                                                    <p>{item.title}</p>
31                                                    <p className='comment'>
32                                                        <Link to={`/comments/${task[1].title}/${item.id}`}>
33                                                            {item.comments.length > 0
34                                                                ? `View Comments`
35                                                                : "Add Comment"}
36                                                        </Link>
37                                                    </p>
38                                                </div>
39                                            )}
40                                        </Draggable>
41                                    ))}
42                                    {provided.placeholder}
43                                </div>
44                            )}
45                        </Droppable>
46                    </div>
47                </div>
48            ))}
49        </DragDropContext>
50    </div>
51);
  • From the code snippet above:
    • The DragDropContext wraps the entire drag-and-drop container, and Droppable represents the parent element for the draggable elements.
    • The Draggable and Droppable components accept a draggable and droppable ID. They also accept a child element, provided – that allows us to reference and render each element as a draggable and droppable item.
    • Feel free to separate the code into different components and click here to learn more about React Beautiful DND.

The DragDropContext accepts a prop onDragEnd, which fires immediately after dragging an element.

1//👇🏻 This function is the value of the onDragEnd prop
2const handleDragEnd = ({ destination, source }) => {
3    if (!destination) return;
4    if (
5        destination.index === source.index &&
6        destination.droppableId === source.droppableId
7    )
8        return;
9
10    socket.emit("taskDragged", {
11        source,
12        destination,
13    });
14};

The code snippet above accepts the destination and source of dragged item, checks if it was dragged to a droppable destination, and if the source and the destination are not the same before sending a message to the Node.js server via Socket.io.

Create a listener to the taskDragged event on the backend.

1socketIO.on("connection", (socket) => {
2    console.log(`⚡: ${socket.id} user just connected!`);
3
4    socket.on("taskDragged", (data) => {
5        console.log(data);
6    });
7
8    socket.on("disconnect", () => {
9        socket.disconnect();
10        console.log("🔥: A user disconnected");
11    });
12});

Let’s briefly examine the data returned after dragging an item:

TestGif

The code snippet below shows that the item moved from the Pending category to the Ongoing category. The index also changed from 0 to 1.

1{
2  source: { index: 0, droppableId: 'pending' },
3  destination: { droppableId: 'ongoing', index: 1 }
4}

Next, make the dragged item remain at its destination. Update the taskDragged listener as below:

1socket.on("taskDragged", (data) => {
2    const { source, destination } = data;
3
4    //👇🏻 Gets the item that was dragged
5    const itemMoved = {
6        ...tasks[source.droppableId].items[source.index],
7    };
8    console.log("DraggedItem>>> ", itemMoved);
9
10    //👇🏻 Removes the item from the its source
11    tasks[source.droppableId].items.splice(source.index, 1);
12
13    //👇🏻 Add the item to its destination using its destination index
14    tasks[destination.droppableId].items.splice(destination.index, 0, itemMoved);
15
16    //👇🏻 Sends the updated tasks object to the React app
17    socket.emit("tasks", tasks);
18
19    /* 👇🏻 Print the items at the Source and Destination
20        console.log("Source >>>", tasks[source.droppableId].items);
21        console.log("Destination >>>", tasks[destination.droppableId].items);
22        */
23});

Create a listener for the tasks event within the TasksContainer component.

1useEffect(() => {
2    socket.on("tasks", (data) => setTasks(data));
3}, [socket]);

Dragndrop

Congratulations!🎉 You can now drag and drop items from one category to another.

How to create new tasks

In this section, I’ll guide you through creating new tasks from the React app.

Update the AddTask.js file to send the new task to the backend server.

1import React, { useState } from "react";
2
3const AddTask = ({ socket }) => {
4    const [task, setTask] = useState("");
5
6    const handleAddTodo = (e) => {
7        e.preventDefault();
8        //👇🏻 sends the task to the Socket.io server
9        socket.emit("createTask", { task });
10        setTask("");
11    };
12    return (
13        <form className='form__input' onSubmit={handleAddTodo}>
14            <label htmlFor='task'>Add Todo</label>
15            <input
16                type='text'
17                name='task'
18                id='task'
19                value={task}
20                className='input'
21                required
22                onChange={(e) => setTask(e.target.value)}
23            />
24            <button className='addTodoBtn'>ADD TODO</button>
25        </form>
26    );
27};
28
29export default AddTask;

Create a listener for the createTask event on the backend server and add the item to the tasks object.

1socketIO.on("connection", (socket) => {
2    console.log(`⚡: ${socket.id} user just connected!`);
3
4    socket.on("createTask", (data) => {
5        // 👇🏻 Constructs an object according to the data structure
6        const newTask = { id: fetchID(), title: data.task, comments: [] };
7        // 👇🏻 Adds the task to the pending category
8        tasks["pending"].items.push(newTask);
9        /* 
10        👇🏻 Fires the tasks event for update
11         */
12        socket.emit("tasks", tasks);
13    });
14    //...other listeners
15});

Completing the Comments page

In this section, you’ll learn how to add and retrieve comments on each task.

Update the Comments.js file as below:

1import React, { useEffect, useState } from "react";
2import socketIO from "socket.io-client";
3import { useParams } from "react-router-dom";
4
5const socket = socketIO.connect("http://localhost:4000");
6
7const Comments = () => {
8    const { category, id } = useParams();
9    const [comment, setComment] = useState("");
10
11    const addComment = (e) => {
12        e.preventDefault();
13        /*
14        👇🏻 sends the comment, the task category, item's id and the userID.
15         */
16        socket.emit("addComment", {
17            comment,
18            category,
19            id,
20            userId: localStorage.getItem("userId"),
21        });
22        setComment("");
23    };
24
25    return (
26        <div className='comments__container'>
27            <form className='comment__form' onSubmit={addComment}>
28                <label htmlFor='comment'>Add a comment</label>
29                <textarea
30                    placeholder='Type your comment...'
31                    value={comment}
32                    onChange={(e) => setComment(e.target.value)}
33                    rows={5}
34                    id='comment'
35                    name='comment'
36                    required
37                ></textarea>
38                <button className='commentBtn'>ADD COMMENT</button>
39            </form>
40            <div className='comments__section'>
41                <h2>Existing Comments</h2>
42                <div></div>
43            </div>
44        </div>
45    );
46};
47
48export default Comments;

Recall that the route for the Comments page is /comments/:category/:id; the code snippet above retrieves the item’s category and its ID from the page’s URL, then sends the item’s category, ID, user ID, and the comment to the Node.js server.

Next, create an event listener on the Node.js server that adds the comment to the specific task via its ID.

1socket.on("addComment", (data) => {
2    const { category, userId, comment, id } = data;
3    //👇🏻 Gets the items in the task's category
4    const taskItems = tasks[category].items;
5    //👇🏻 Loops through the list of items to find a matching ID
6    for (let i = 0; i < taskItems.length; i++) {
7        if (taskItems[i].id === id) {
8    //👇🏻 Then adds the comment to the list of comments under the item (task)
9            taskItems[i].comments.push({
10                name: userId,
11                text: comment,
12                id: fetchID(),
13            });
14            //👇🏻 sends a new event to the React app
15            socket.emit("comments", taskItems[i].comments);
16        }
17    }
18});

Fetch the comments via Socket.io.

1const Comments = () => {
2    const { category, id } = useParams();
3    const [comment, setComment] = useState("");
4    const [commentList, setCommentList] = useState([]);
5
6    //👇🏻 Listens to the comments event
7    useEffect(() => {
8        socket.on("comments", (data) => setCommentList(data));
9    }, []);
10
11    //...other listeners
12    return (
13        <div className='comments__container'>
14            <form className='comment__form' onSubmit={addComment}>
15                ...
16            </form>
17
18            {/** 👇🏻 Displays all the available comments*/}
19            <div className='comments__section'>
20                <h2>Existing Comments</h2>
21                {commentList.map((comment) => (
22                    <div key={comment.id}>
23                        <p>
24                            <span style={{ fontWeight: "bold" }}>{comment.text} </span>by{" "}
25                            {comment.name}
26                        </p>
27                    </div>
28                ))}
29            </div>
30        </div>
31    );
32};
33
34export default Comments;

Lastly, add this useEffect hook to fetch the comments when the page is loaded to the browser.

1useEffect(() => {
2    socket.emit("fetchComments", { category, id });
3}, [category, id]);

Listen to the event on the backend and trigger the comments event to return the list of comments matching the item’s ID and category.

1socket.on("fetchComments", (data) => {
2    const { category, id } = data;
3    const taskItems = tasks[category].items;
4    for (let i = 0; i < taskItems.length; i++) {
5        if (taskItems[i].id === id) {
6            socket.emit("comments", taskItems[i].comments);
7        }
8    }
9});

Congratulations!💃🏻 We’ve completed this project.

EXTRA: Sending notifications with Novu

If you want to add notifications to the application when a user adds a comment or a new task, 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.

How to add Novu to a React and Node.js application

To add the in-app notification, 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👇🏻 Install on the client
2npx novu init
3

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? Devto Clone
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.

Image description

Update the components/Nav.js file to contain Novu and its required elements 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 Nav = () => {
10    const navigate = useNavigate();
11
12    const onNotificationClick = (notification) =>
13        navigate(notification.cta.data.url);
14    return (
15        <nav className='navbar'>
16            <h3>Team's todo list</h3>
17            <div>
18                <NovuProvider
19                    subscriberId='<SUBSCRIBER_ID>'
20                    applicationIdentifier='<APP_ID>'
21                >
22                    <PopoverNotificationCenter
23                        onNotificationClick={onNotificationClick}
24                        colorScheme='light'
25                    >
26                        {({ unseenCount }) => (
27                            <NotificationBell unseenCount={unseenCount} />
28                        )}
29                    </PopoverNotificationCenter>
30                </NovuProvider>
31            </div>
32        </nav>
33    );
34};
35
36export 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.

Image description

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:

Image description

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 and edit the notification template to contain the content below.

1{{userId}} added a new task.

💡 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.

Adding Novu to the application

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

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

Create a function that sends the notification via Novu to the React app.

1const sendNotification = async (user) => {
2    try {
3        const result = await novu.trigger(<TEMPLATE_ID>, {
4            to: {
5                subscriberId: <SUBSCRIBER_ID>,
6            },
7            payload: {
8                userId: user,
9            },
10        });
11        console.log(result);
12    } catch (err) {
13        console.error("Error >>>>", { err });
14    }
15};
16
17//👇🏻 The function is called after a new task is created
18socket.on("createTask", (data) => {
19        const newTask = { id: fetchID(), title: data.task, comments: [] };
20        tasks["pending"].items.push(newTask);
21        socket.emit("tasks", tasks);
22//👇🏻 Triggers the notification via Novu
23        sendNotification(data.userId);
24    });

The code snippet above sends a notification to all users when a new task is added to the application.

Novu

Conclusion

So far, you’ve learnt how to set up Socket.io in a React and Node.js application, communicate between a server and a client via Socket.io, and drag and drop items with React Beautiful DND.

This is a demo of what you can build using Socket.io and React Beautiful DND. Feel free to improve the application by adding authentication, the ability to assign tasks to a particular user, and add notifications when a user drops a comment.

The source code for this tutorial is available here: https://github.com/novuhq/blog/tree/main/react-beautiful-dnd-todo-list

Trello

Thank you for reading!

P.S Novu is sending awesome swag on Hacktoberfest! Happy if you could support us by giving us a star! ⭐️

https://github.com/novuhq/novu

Related Posts

category: How to

A Proper Guide to Web and Mobile Push Notification Service

Implement push notification services successfully by following this actionable guide on choosing platforms, setting up, and personalizing notifications for better results.

Emil Pearce
Emil Pearce
category: How to

How to Add Real-Time Notifications to a React App

Learn how to integrate real-time notifications into your React app using WebSockets, Server-Sent Events, Firebase Cloud Messaging (FCM), and Novu for improved user engagement and instant updates.

Emil Pearce
Emil Pearce
category: How to

A Developer’s Guide to Choosing the Best Notification Platform

A comprehensive guide for developers on selecting notification platforms, covering different types of notifications (push, in-app, email, SMS, and chat), key features to consider, popular providers, and best practices for implementation. Learn how to evaluate notification platforms based on integration ease, cost-effectiveness, scalability, and security, with practical insights on platforms like FCM, Twilio, SendGrid, and Novu.

Emil Pearce
Emil Pearce