Building a forum with React, NodeJS
In this article, you'll learn how to build a forum system that allows users to create, react, and reply to post threads. In the end, we will also send a notification on each reply on a thread with Novu, you can skip the last step if you want only the technical stuff.
I know, there are no forums today like before, it’s all about the Reddit, Facebook communities (and smaller ones like Devto and Mastodon), but! Once upon a time, when I was a boy, I was addicted to forums, I have actually created a few with PHPBB and vBulletin, back when PHP was a thing. being so nostalgic made me write this blog post 😎
A small request 🤗
I am trying to get Novu to 20k stars, can you help me out by starring the repository? it helps me to create more content every week.
https://github.com/novuhq/novu
Project Setup
Here, I’ll guide you through creating the project environment for the web application. We’ll use React.js for the front end and Node.js for the backend server.
Create the project folder for the web application by running the code below:
1mkdir forum-system
2cd forum-system
3mkdir client server
Setting up the Node.js server
Navigate into the server folder and create a package.json
file.
1cd server & npm init -y
Install Express, Nodemon, and the CORS library.
1npm install express cors nodemon
ExpressJS 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, and Nodemon is a Node.js tool that automatically restarts the server after detecting file changes.
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 cors = require("cors");
4const app = express();
5const PORT = 4000;
6
7app.use(express.urlencoded({ extended: true }));
8app.use(express.json());
9app.use(cors());
10
11app.get("/api", (req, res) => {
12 res.json({
13 message: "Hello world",
14 });
15});
16
17app.listen(PORT, () => {
18 console.log(`Server listening on ${PORT}`);
19});
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 },
Congratulations! You can now start the server by using the command below.
1npm start
Setting up the React application
Navigate into the client folder via your terminal and create a new React.js project.
1cd client
2npx create-react-app ./
Install React Router – a JavaScript library that enables us to navigate between pages in a React application.
1npm install 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 done below.
1function App() {
2 return (
3 <div>
4 <p>Hello World!</p>
5 </div>
6 );
7}
8export default App;
Copy the CSS file required for styling the project here into the src/index.css
file.
Building the app user interface
Here, we’ll create the user interface for the forum system to enable users to create, reply and react to various threads.
Create a components folder within the client/src
folder containing the Home.js
, Login.js
, Nav.js
, Register.js
, and Replies.js
files.
1cd client/src
2mkdir components
3touch Home.js Login.js Nav.js Register.js Replies.js
- From the code snippet above:
- The
Login.js
andRegister.js
files are the authentication pages of the web application. - The
Home.js
file represents the dashboard page shown after authentication. It allows users to create and react to the post threads. - The
Replies.js
file displays the response on each post and allows users to reply to the post thread. - The
Nav.js
is the navigation bar where we will configure Novu.
- The
Update the App.js
file to render the components using React Router
1import React from "react";
2import { BrowserRouter, Route, Routes } from "react-router-dom";
3import Register from "./components/Register";
4import Login from "./components/Login";
5import Home from "./components/Home";
6import Replies from "./components/Replies";
7
8const App = () => {
9 return (
10 <div>
11 <BrowserRouter>
12 <Routes>
13 <Route path='/' element={<Login />} />
14 <Route path='/register' element={<Register />} />
15 <Route path='/dashboard' element={<Home />} />
16 <Route path='/:id/replies' element={<Replies />} />
17 </Routes>
18 </BrowserRouter>
19 </div>
20 );
21};
22
23export default App;
The Login page
Copy the code below into the Login.js
file to render a form that accepts the user’s email and password.
1import React, { useState } from "react";
2import { Link, useNavigate } from "react-router-dom";
3
4const Login = () => {
5 const [email, setEmail] = useState("");
6 const [password, setPassword] = useState("");
7
8 const handleSubmit = (e) => {
9 e.preventDefault();
10 console.log({ email, password });
11 setEmail("");
12 setPassword("");
13 };
14
15 return (
16 <main className='login'>
17 <h1 className='loginTitle'>Log into your account</h1>
18 <form className='loginForm' onSubmit={handleSubmit}>
19 <label htmlFor='email'>Email Address</label>
20 <input
21 type='text'
22 name='email'
23 id='email'
24 required
25 value={email}
26 onChange={(e) => setEmail(e.target.value)}
27 />
28 <label htmlFor='password'>Password</label>
29 <input
30 type='password'
31 name='password'
32 id='password'
33 required
34 value={password}
35 onChange={(e) => setPassword(e.target.value)}
36 />
37 <button className='loginBtn'>SIGN IN</button>
38 <p>
39 Don't have an account? <Link to='/register'>Create one</Link>
40 </p>
41 </form>
42 </main>
43 );
44};
45export default Login;
The Register page
Update the Register.js
file to display the registration form that allows users to create an account using their email, username, and password.
1import React, { useState } from "react";
2import { Link, useNavigate } from "react-router-dom";
3
4const Register = () => {
5 const [username, setUsername] = useState("");
6 const [email, setEmail] = useState("");
7 const [password, setPassword] = useState("");
8
9 const handleSubmit = (e) => {
10 e.preventDefault();
11 console.log({ username, email, password });
12 setEmail("");
13 setUsername("");
14 setPassword("");
15 };
16 return (
17 <main className='register'>
18 <h1 className='registerTitle'>Create an account</h1>
19 <form className='registerForm' onSubmit={handleSubmit}>
20 <label htmlFor='username'>Username</label>
21 <input
22 type='text'
23 name='username'
24 id='username'
25 required
26 value={username}
27 onChange={(e) => setUsername(e.target.value)}
28 />
29 <label htmlFor='email'>Email Address</label>
30 <input
31 type='text'
32 name='email'
33 id='email'
34 required
35 value={email}
36 onChange={(e) => setEmail(e.target.value)}
37 />
38 <label htmlFor='password'>Password</label>
39 <input
40 type='password'
41 name='password'
42 id='password'
43 required
44 value={password}
45 onChange={(e) => setPassword(e.target.value)}
46 />
47 <button className='registerBtn'>REGISTER</button>
48 <p>
49 Have an account? <Link to='/'>Sign in</Link>
50 </p>
51 </form>
52 </main>
53 );
54};
55
56export default Register;
The Nav component
Update the Nav.js
file to render a navigation bar that shows the application’s title and a sign-out button. Later in this article, I’ll guide you through adding Novu’s notification bell within this component.
1import React from "react";
2
3const Nav = () => {
4 const signOut = () => {
5 alert("User signed out!");
6 };
7 return (
8 <nav className='navbar'>
9 <h2>Threadify</h2>
10 <div className='navbarRight'>
11 <button onClick={signOut}>Sign out</button>
12 </div>
13 </nav>
14 );
15};
16
17export default Nav;
The Home page
It is the home page after the user’s authentication. They can create and react to posts (threads) within the Home component. Copy the code below into the Home.js
file.
1import React, { useState } from "react";
2import Nav from "./Nav";
3
4const Home = () => {
5 const [thread, setThread] = useState("");
6
7 const handleSubmit = (e) => {
8 e.preventDefault();
9 console.log({ thread });
10 setThread("");
11 };
12 return (
13 <>
14 <Nav />
15 <main className='home'>
16 <h2 className='homeTitle'>Create a Thread</h2>
17 <form className='homeForm' onSubmit={handleSubmit}>
18 <div className='home__container'>
19 <label htmlFor='thread'>Title / Description</label>
20 <input
21 type='text'
22 name='thread'
23 required
24 value={thread}
25 onChange={(e) => setThread(e.target.value)}
26 />
27 </div>
28 <button className='homeBtn'>CREATE THREAD</button>
29 </form>
30 </main>
31 </>
32 );
33};
34
35export default Home;
The code snippet above displays the navigation bar and an input field for the posts. We’ll update the component in the upcoming sections.
The Replies page
This page is a dynamic route that allows users to reply and view comments on a post thread. Update the Replies.js
file with the code snippet below:
1import React, { useState } from "react";
2
3const Replies = () => {
4 const [reply, setReply] = useState("");
5
6 const handleSubmitReply = (e) => {
7 e.preventDefault();
8 console.log({ reply });
9 setReply("");
10 };
11
12 return (
13 <main className='replies'>
14 <form className='modal__content' onSubmit={handleSubmitReply}>
15 <label htmlFor='reply'>Reply to the thread</label>
16 <textarea
17 rows={5}
18 value={reply}
19 onChange={(e) => setReply(e.target.value)}
20 type='text'
21 name='reply'
22 className='modalInput'
23 />
24
25 <button className='modalBtn'>SEND</button>
26 </form>
27 </main>
28 );
29};
30
31export default Replies;
Congratulations! You’ve designed the application’s user interface. Next, you’ll learn how to register and log users into the application.
User authentication with React and Node.js
Here, I’ll guide you through authenticating users and how to allow only authorised users to access protected pages within the web application.
PS: In a real-world application, passwords are hashed and saved in a secure database. For simplicity purposes, I’ll store all the credentials in an array in this tutorial.
Creating new users
Add a POST route on the server that accepts the user’s credentials – email, username, and password.
1//👇🏻 holds all the existing users
2const users = [];
3//👇🏻 generates a random string as ID
4const generateID = () => Math.random().toString(36).substring(2, 10);
5
6app.post("/api/register", async (req, res) => {
7 const { email, password, username } = req.body;
8 //👇🏻 holds the ID
9 const id = generateID();
10 //👇🏻 logs all the user's credentials to the console.
11 console.log({ email, password, username, id });
12});
Create a signUp
function within the Register.js
file that sends the user’s credentials to the endpoint on the server.
1const signUp = () => {
2 fetch("http://localhost:4000/api/register", {
3 method: "POST",
4 body: JSON.stringify({
5 email,
6 password,
7 username,
8 }),
9 headers: {
10 "Content-Type": "application/json",
11 },
12 })
13 .then((res) => res.json())
14 .then((data) => {
15 console.log(data);
16 })
17 .catch((err) => console.error(err));
18};
Call the function when a user submits the form as done below:
1const handleSubmit = (e) => {
2 e.preventDefault();
3 //👇🏻 Triggers the function
4 signUp();
5 setEmail("");
6 setUsername("");
7 setPassword("");
8};
Update the /api/register
route to save the user’s credentials and return a response to the front-end.
1app.post("/api/register", async (req, res) => {
2 const { email, password, username } = req.body;
3 const id = generateID();
4 //👇🏻 ensures there is no existing user with the same credentials
5 const result = users.filter(
6 (user) => user.email === email && user.password === password
7 );
8 //👇🏻 if true
9 if (result.length === 0) {
10 const newUser = { id, email, password, username };
11 //👇🏻 adds the user to the database (array)
12 users.push(newUser);
13 //👇🏻 returns a success message
14 return res.json({
15 message: "Account created successfully!",
16 });
17 }
18 //👇🏻 if there is an existing user
19 res.json({
20 error_message: "User already exists",
21 });
22});
The code snippet above accepts the user’s credentials from the React.js application and checks if there is no existing user with the same credentials before saving the user to the database (array).
Finally, display the server’s response by updating the signUp
function as done below.
1//👇🏻 React Router's useNavigate hook
2const navigate = useNavigate();
3
4const signUp = () => {
5 fetch("http://localhost:4000/api/register", {
6 method: "POST",
7 body: JSON.stringify({
8 email,
9 password,
10 username,
11 }),
12 headers: {
13 "Content-Type": "application/json",
14 },
15 })
16 .then((res) => res.json())
17 .then((data) => {
18 if (data.error_message) {
19 alert(data.error_message);
20 } else {
21 alert("Account created successfully!");
22 navigate("/");
23 }
24 })
25 .catch((err) => console.error(err));
26};
The code snippet above displays a success or error message from the Node.js server and redirects the user to the login page after successfully creating the account.
Logging users into the application
Add a POST route on the server that accepts the user’s email and password and authenticates the users before granting them access to the web application.
1app.post("/api/login", (req, res) => {
2 const { email, password } = req.body;
3 //👇🏻 checks if the user exists
4 let result = users.filter(
5 (user) => user.email === email && user.password === password
6 );
7 //👇🏻 if the user doesn't exist
8 if (result.length !== 1) {
9 return res.json({
10 error_message: "Incorrect credentials",
11 });
12 }
13 //👇🏻 Returns the id if successfuly logged in
14 res.json({
15 message: "Login successfully",
16 id: result[0].id,
17 });
18});
Create a loginUser
function within the Login.js
file that sends the user’s email and password to the Node.js server.
1//👇🏻 React Router's useNavigate hook
2const navigate = useNavigate();
3
4const loginUser = () => {
5 fetch("http://localhost:4000/api/login", {
6 method: "POST",
7 body: JSON.stringify({
8 email,
9 password,
10 }),
11 headers: {
12 "Content-Type": "application/json",
13 },
14 })
15 .then((res) => res.json())
16 .then((data) => {
17 if (data.error_message) {
18 alert(data.error_message);
19 } else {
20 alert(data.message);
21 navigate("/dashboard");
22 localStorage.setItem("_id", data.id);
23 }
24 })
25 .catch((err) => console.error(err));
26};
The code snippet above sends the user’s credentials to the Node.js server and displays the response on the front end. The application redirects authenticated users to the Home
component and saves their id
to the local storage for easy identification.
Update the signOut
function within the Nav.js
file to remove the id
from the local storage when a user logs out.
1const signOut = () => {
2 localStorage.removeItem("_id");
3 //👇🏻 redirects to the login page
4 navigate("/");
5};
Creating and retrieving post threads within the application
Here, you’ll learn how to create and retrieve the posts from the Node.js server.
Add a POST route within the index.js
file that accepts the post title and the user’s id from the React.js application.
1app.post("/api/create/thread", async (req, res) => {
2 const { thread, userId } = req.body;
3 const threadId = generateID();
4
5 console.log({ thread, userId, threadId });
6});
Next, send the user’s id and the post title to the server. Before then, let’s ensure the Home.js
route is protected. Add a useEffect
hook within the Home component to determine whether the user is authenticated.
1import React, { useEffect, useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Home = () => {
5 const navigate = useNavigate();
6
7 //👇🏻 The useEffect Hook
8 useEffect(() => {
9 const checkUser = () => {
10 if (!localStorage.getItem("_id")) {
11 navigate("/");
12 } else {
13 console.log("Authenticated");
14 }
15 };
16 checkUser();
17 }, [navigate]);
18
19 return <>{/*--the UI elements*/}</>;
20};
21
When users sign in to the application, we save their id
to the local storage for easy identification. The code snippet above checks if the id
exists; otherwise, the user is redirected to the login page.
Add a function within the Home component that sends the user’s id and the post title to the Node.js server when the form is submitted.
1const createThread = () => {
2 fetch("http://localhost:4000/api/create/thread", {
3 method: "POST",
4 body: JSON.stringify({
5 thread,
6 userId: localStorage.getItem("_id"),
7 }),
8 headers: {
9 "Content-Type": "application/json",
10 },
11 })
12 .then((res) => res.json())
13 .then((data) => {
14 console.log(data);
15 })
16 .catch((err) => console.error(err));
17};
18
19//👇🏻 Triggered when the form is submitted
20const handleSubmit = (e) => {
21 e.preventDefault();
22 //👇🏻 Calls the function
23 createThread();
24 setThread("");
25};
Save the post and send all the available posts to the client for display.
1//👇🏻 holds all the posts created
2const threadList = [];
3
4app.post("/api/create/thread", async (req, res) => {
5const { thread, userId } = req.body;
6const threadId = generateID();
7
8 //👇🏻 add post details to the array
9 threadList.unshift({
10 id: threadId,
11 title: thread,
12 userId,
13 replies: [],
14 likes: [],
15 });
16
17 //👇🏻 Returns a response containing the posts
18 res.json({
19 message: "Thread created successfully!",
20 threads: threadList,
21 });
22});
The code snippet above accepts the user’s id and post title from the front end. Then, save an object that holds the post details and returns a response containing all the saved posts.
Displaying the post threads
Create a state that will hold all the posts within the Home component.
1const [threadList, setThreadList] = useState([]);
Update the createThread
function as done below:
1const createThread = () => {
2 fetch("http://localhost:4000/api/create/thread", {
3 method: "POST",
4 body: JSON.stringify({
5 thread,
6 userId: localStorage.getItem("_id"),
7 }),
8 headers: {
9 "Content-Type": "application/json",
10 },
11 })
12 .then((res) => res.json())
13 .then((data) => {
14 alert(data.message);
15 setThreadList(data.threads);
16 })
17 .catch((err) => console.error(err));
18};
The createThread
function retrieves all the posts available within the application and saves them into the threadList
state.
Update the Home.js
file to display the available posts as done below:
1return (
2 <>
3 <Nav />
4 <main className='home'>
5 <h2 className='homeTitle'>Create a Thread</h2>
6 <form className='homeForm' onSubmit={handleSubmit}>
7 {/*--form UI elements--*/}
8 </form>
9
10 <div className='thread__container'>
11 {threadList.map((thread) => (
12 <div className='thread__item' key={thread.id}>
13 <p>{thread.title}</p>
14 <div className='react__container'>
15 <Likes numberOfLikes={thread.likes.length} threadId={thread.id} />
16 <Comments
17 numberOfComments={thread.replies.length}
18 threadId={thread.id}
19 title={thread.title}
20 />
21 </div>
22 </div>
23 ))}
24 </div>
25 </main>
26 </>
27);
- From the code snippet above:
- All the posts are displayed within the Home component.
- I added two new components – Likes and Comments. They both contain SVG icons from Heroicons.
- The
Likes
component accepts the post id and the number of likes on a post – length of the likes array on each post. - The
Comments
component accepts the length of thereplies
array, the post id, and its title.
Create a utils
folder containing both components.
1cd client/src
2mkdir utils
3touch Likes.js Comments.js
Copy the code below into the Likes.js
file.
1import React from "react";
2
3const Likes = ({ numberOfLikes, threadId }) => {
4 return (
5 <div className='likes__container'>
6 <svg
7 xmlns='http://www.w3.org/2000/svg'
8 viewBox='0 0 24 24'
9 fill='currentColor'
10 className='w-4 h-4 likesBtn'
11 >
12 <path d='M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z' />
13 </svg>
14 <p style={{ color: "#434242" }}>
15 {numberOfLikes === 0 ? "" : numberOfLikes}
16 </p>
17 </div>
18 );
19};
20
21export default Likes;
The code snippet above contains the SVG element for displaying the Like icon. The component also renders the number of likes on a post.
Copy the code below into the Comments.js
file.
1import React from "react";
2import { useNavigate } from "react-router-dom";
3
4const Comments = ({ numberOfComments, threadId }) => {
5 const navigate = useNavigate();
6
7 const handleAddComment = () => {
8 navigate(`/${threadId}/replies`);
9 };
10
11 return (
12 <div className='likes__container'>
13 <svg
14 xmlns='http://www.w3.org/2000/svg'
15 viewBox='0 0 24 24'
16 fill='currentColor'
17 className='w-6 h-6 likesBtn'
18 onClick={handleAddComment}
19 >
20 <path
21 fillRule='evenodd'
22 d='M4.804 21.644A6.707 6.707 0 006 21.75a6.721 6.721 0 003.583-1.029c.774.182 1.584.279 2.417.279 5.322 0 9.75-3.97 9.75-9 0-5.03-4.428-9-9.75-9s-9.75 3.97-9.75 9c0 2.409 1.025 4.587 2.674 6.192.232.226.277.428.254.543a3.73 3.73 0 01-.814 1.686.75.75 0 00.44 1.223zM8.25 10.875a1.125 1.125 0 100 2.25 1.125 1.125 0 000-2.25zM10.875 12a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0zm4.875-1.125a1.125 1.125 0 100 2.25 1.125 1.125 0 000-2.25z'
23 clipRule='evenodd'
24 />
25 </svg>
26 <p style={{ color: "#434242" }}>
27 {numberOfComments === 0 ? "" : numberOfComments}
28 </p>
29 </div>
30 );
31};
32
33export default Comments;
The code snippet contains the SVG element for the Comments button and the number of comments on the post.
The handleAddComment
function is triggered when a user clicks the comment icon. It redirects the user to the Replies
component where they can view and add to the replies on each post.
So far, we’ve been able to display the available posts only when we create a new post. Next, let’s retrieve them when the component is mounted.
Add a GET route on the server that returns all the posts.
1app.get("/api/all/threads", (req, res) => {
2 res.json({
3 threads: threadList,
4 });
5});
Update the useEffect
hook within the Home component to fetch all the posts from the server.
1useEffect(() => {
2 const checkUser = () => {
3 if (!localStorage.getItem("_id")) {
4 navigate("/");
5 } else {
6 fetch("http://localhost:4000/api/all/threads")
7 .then((res) => res.json())
8 .then((data) => setThreadList(data.threads))
9 .catch((err) => console.error(err));
10 }
11 };
12 checkUser();
13}, [navigate]);
Congratulations on making it this far! Next, you’ll learn how to react and reply to the posts.
How to react and reply to each posts
In this section, you’ll learn how to react and reply to each post. Users will be able to drop a like or comment on each post.
Reacting to each post
Create a function within the Likes.js
file that runs when a user clicks on the Like icon.
1const Likes = ({ numberOfLikes, threadId }) => {
2 const handleLikeFunction = () => {
3 alert("You just liked the post!");
4 };
5
6 return (
7 <div className='likes__container'>
8 <svg
9 xmlns='http://www.w3.org/2000/svg'
10 viewBox='0 0 24 24'
11 fill='currentColor'
12 className='w-4 h-4 likesBtn'
13 onClick={handleLikeFunction}
14 >
15 {/*--other UI elements*/}
16 </svg>
17 </div>
18 );
19};
Create a POST route on the server that validates the reaction.
1app.post("/api/thread/like", (req, res) => {
2 //👇🏻 accepts the post id and the user id
3 const { threadId, userId } = req.body;
4 //👇🏻 gets the reacted post
5 const result = threadList.filter((thread) => thread.id === threadId);
6 //👇🏻 gets the likes property
7 const threadLikes = result[0].likes;
8 //👇🏻 authenticates the reaction
9 const authenticateReaction = threadLikes.filter((user) => user === userId);
10 //👇🏻 adds the users to the likes array
11 if (authenticateReaction.length === 0) {
12 threadLikes.push(userId);
13 return res.json({
14 message: "You've reacted to the post!",
15 });
16 }
17 //👇🏻 Returns an error user has reacted to the post earlier
18 res.json({
19 error_message: "You can only react once!",
20 });
21});
- From the code snippet above:
- The route accepts the post id and the user’s id from the React.js application and searches for the post that received a reaction.
- Then, validates the reaction before adding the user to the likes array.
Update the handleLikeFunction
to send a POST request to the api/thread/like
endpoint whenever a user reacts to the post.
1const handleLikeFunction = () => {
2 fetch("http://localhost:4000/api/thread/like", {
3 method: "POST",
4 body: JSON.stringify({
5 threadId,
6 userId: localStorage.getItem("_id"),
7 }),
8 headers: {
9 "Content-Type": "application/json",
10 },
11 })
12 .then((res) => res.json())
13 .then((data) => {
14 if (data.error_message) {
15 alert(data.error_message);
16 } else {
17 alert(data.message);
18 }
19 })
20 .catch((err) => console.error(err));
21};
Displaying the replies on each post
Here, I’ll guide you through displaying the replies on each post within the React app.
Add a POST route on the server that accepts a post ID from the front end and returns the title and all the responses under such post.
1app.post("/api/thread/replies", (req, res) => {
2 //👇🏻 The post ID
3 const { id } = req.body;
4 //👇🏻 searches for the post
5 const result = threadList.filter((thread) => thread.id === id);
6 //👇🏻 return the title and replies
7 res.json({
8 replies: result[0].replies,
9 title: result[0].title,
10 });
11});
Next, update the Replies component to send a request to the api/thread/replies
endpoint on the server and display the title and replies when the page loads.
1import React, { useEffect, useState } from "react";
2import { useParams, useNavigate } from "react-router-dom";
3
4const Replies = () => {
5 const [replyList, setReplyList] = useState([]);
6 const [reply, setReply] = useState("");
7 const [title, setTitle] = useState("");
8 const navigate = useNavigate();
9 const { id } = useParams();
10
11 useEffect(() => {
12 const fetchReplies = () => {
13 fetch("http://localhost:4000/api/thread/replies", {
14 method: "POST",
15 body: JSON.stringify({
16 id,
17 }),
18 headers: {
19 "Content-Type": "application/json",
20 },
21 })
22 .then((res) => res.json())
23 .then((data) => {
24 setReplyList(data.replies);
25 setTitle(data.title);
26 })
27 .catch((err) => console.error(err));
28 };
29 fetchReplies();
30 }, [id]);
31
32 //👇🏻 This function when triggered when we add a new reply
33 const handleSubmitReply = (e) => {
34 e.preventDefault();
35 console.log({ reply });
36 setReply("");
37 };
38 return <main className='replies'>{/*--UI elements--*/}</main>;
39};
- From the code snippet above:
- The
useEffect
hook sends a request to the server’s endpoint and retrieves the post title and its replies. - The
title
andreplyList
states hold both the title and replies respectively. - The variable
id
holds the post ID retrieved from the URL via dynamic routing in React Router.
- The
Display the replies on each post as done below:
1return (
2 <main className='replies'>
3 <h1 className='repliesTitle'>{title}</h1>
4
5 <form className='modal__content' onSubmit={handleSubmitReply}>
6 <label htmlFor='reply'>Reply to the thread</label>
7 <textarea
8 rows={5}
9 value={reply}
10 onChange={(e) => setReply(e.target.value)}
11 type='text'
12 name='reply'
13 className='modalInput'
14 />
15
16 <button className='modalBtn'>SEND</button>
17 </form>
18
19 <div className='thread__container'>
20 {replyList.map((reply) => (
21 <div className='thread__item'>
22 <p>{reply.text}</p>
23 <div className='react__container'>
24 <p style={{ opacity: "0.5" }}>by {reply.name}</p>
25 </div>
26 </div>
27 ))}
28 </div>
29 </main>
30);
The code snippet above shows the layout of the Replies component displaying the post title, replies, and a form field for replying to a post.
Creating the post reply functionality
Create an endpoint on the server that allows users to add new replies as done below:
1app.post("/api/create/reply", async (req, res) => {
2 //👇🏻 accepts the post id, user id, and reply
3 const { id, userId, reply } = req.body;
4 //👇🏻 search for the exact post that was replied to
5 const result = threadList.filter((thread) => thread.id === id);
6 //👇🏻 search for the user via its id
7 const user = users.filter((user) => user.id === userId);
8 //👇🏻 saves the user name and reply
9 result[0].replies.unshift({
10 userId: user[0].id,
11 name: user[0].username,
12 text: reply,
13 });
14
15 res.json({
16 message: "Response added successfully!",
17 });
18});
19
The code snippet above accepts the post id, user id, and reply from the React app, searches for the post via its ID, and adds the user’s id, username, and reply to the post replies.
Create a function that sends a request to the /api/create/reply
endpoint.
1const addReply = () => {
2 fetch("http://localhost:4000/api/create/reply", {
3 method: "POST",
4 body: JSON.stringify({
5 id,
6 userId: localStorage.getItem("_id"),
7 reply,
8 }),
9 headers: {
10 "Content-Type": "application/json",
11 },
12 })
13 .then((res) => res.json())
14 .then((data) => {
15 alert(data.message);
16 navigate("/dashboard");
17 })
18 .catch((err) => console.error(err));
19};
20
21const handleSubmitReply = (e) => {
22 e.preventDefault();
23 //👇🏻 calls the function
24 addReply();
25 setReply("");
26};
Congratulations on making it thus far! In the upcoming sections, you’ll learn how to send notifications to multiple users using the Topics API provided by Novu.
How to send notifications via the Topic API in Novu
In this section, we’ll use the Novu Topic API to send notifications to multiple users simultaneously. To do this, the API allows us to create a unique topic, assign subscribers to the topic, and send a bulk notification to the subscribers at once.
Setting up your Novu admin panel
Navigate into the client folder and create a Novu project by running the code below.
1cd client
2npx novu init
Select your application name and sign in to Novu. 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? Forum App
3? Now lets setup your environment. How would you like to proceed? Create a free cloud account (Recommended)
4? Create your account with: Sign-in with GitHub
5? I accept the Terms and Conditions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy) Yes
6✔ Created your account successfully.
7
8 We've created a demo web page for you to see novu notifications in action.
9 Visit: http://localhost:51422/demo to continue
Visit the demo page, copy your subscriber ID from the page, and click the Skip Tutorial button.
Create a notification template with a workflow as shown below:
Update the In-App notification template to send this message to the post creator when there is a new reply.
1Someone just dropped a reply to the thread!
Install the Novu Notification package within your React project.
1npm install @novu/notification-center
Update the components/Nav.js
file to contain the Novu notification bell according to its documentation.
1import React from "react";
2import {
3 NovuProvider,
4 PopoverNotificationCenter,
5 NotificationBell,
6} from "@novu/notification-center";
7import { useNavigate } from "react-router-dom";
8
9const Nav = () => {
10 const navigate = useNavigate();
11
12 const onNotificationClick = (notification) =>
13 navigate(notification.cta.data.url);
14
15 const signOut = () => {
16 localStorage.removeItem("_id");
17 navigate("/");
18 };
19 return (
20 <nav className='navbar'>
21 <h2>Threadify</h2>
22
23 <div className='navbarRight'>
24 <NovuProvider
25 subscriberId='<YOUR_SUBSCRIBER_ID>'
26 applicationIdentifier='<YOUR_APP_ID>'
27 >
28 <PopoverNotificationCenter
29 onNotificationClick={onNotificationClick}
30 colorScheme='light'
31 >
32 {({ unseenCount }) => (
33 <NotificationBell unseenCount={unseenCount} />
34 )}
35 </PopoverNotificationCenter>
36 </NovuProvider>
37
38 <button onClick={signOut}>Sign out</button>
39 </div>
40 </nav>
41 );
42};
The code snippet above adds Novu’s notification bell icon to the Nav component, enabling us to view all the notifications in our app. Replace the Subscriber ID variable with yours.
Select Settings on your Novu Admin Panel and copy your App ID and API key. Ensure you add the App ID into its variable within the Nav.js
file.
Configuring Novu on the server
Install the Novu SDK for Node.js into the server folder.
1npm install @novu/node
Import Novu from the package and create an instance using your API Key. Replace the API Key variable with your API key copied earlier.
1const { Novu } = require("@novu/node");
2const novu = new Novu("<YOUR_API_KEY>");
Sending notifications via the Novu Topic API
To begin with, you’ll need to add the users as subscribers when they login to the application. Therefore, update the /api/register
route to add the user as a Novu subscriber.
1app.post("/api/register", async (req, res) => {
2 const { email, password, username } = req.body;
3 const id = generateID();
4 const result = users.filter(
5 (user) => user.email === email && user.password === password
6 );
7
8 if (result.length === 0) {
9 const newUser = { id, email, password, username };
10 //👇🏻 add the user as a subscriber
11 await novu.subscribers.identify(id, { email: email });
12
13 users.push(newUser);
14 return res.json({
15 message: "Account created successfully!",
16 });
17 }
18 res.json({
19 error_message: "User already exists",
20 });
21});
The code snippet above creates a Novu subscriber via the user’s email and id.
Next, add each new post as a Novu topic and add the user as a subscriber to the topic.
1app.post("/api/create/thread", async (req, res) => {
2 const { thread, userId } = req.body;
3 let threadId = generateID();
4 threadList.unshift({
5 id: threadId,
6 title: thread,
7 userId,
8 replies: [],
9 likes: [],
10 });
11//👇🏻 creates a new topic from the post
12 await novu.topics.create({
13 key: threadId,
14 name: thread,
15 });
16//👇🏻 add the user as a subscriber
17 await novu.topics.addSubscribers(threadId, {
18 subscribers: [userId],
19 //replace with your subscriber ID to test run
20 // subscribers: ["<YOUR_SUBSCRIBER_ID>"],
21 });
22
23 res.json({
24 message: "Thread created successfully!",
25 threads: threadList,
26 });
27});
Finally, send notifications to the subscriber when there is a new response on the thread
1app.post("/api/create/reply", async (req, res) => {
2 const { id, userId, reply } = req.body;
3 const result = threadList.filter((thread) => thread.id === id);
4 const user = users.filter((user) => user.id === userId);
5 result[0].replies.unshift({ name: user[0].username, text: reply });
6//👇🏻 Triggers the function when there is a new reply
7 await novu.trigger("topicnotification", {
8 to: [{ type: "Topic", topicKey: id }],
9 });
10
11 res.json({
12 message: "Response added successfully!",
13 });
14});
Congratulations!🎉 You’ve completed the project for this tutorial.
Conclusion
So far, you’ve learnt how to authenticate users, communicate between a React and Node.js app, and send bulk notifications using the Novu Topic API.
Novu is an open-source notification infrastructure that enables you to send SMS, chat, push, and e-mail notifications from a single dashboard. The Topic API is just one of the exciting features Novu provides; feel free to learn more about us here.
The source code for this tutorial is available here:
https://github.com/novuhq/blog/tree/main/forum-system-with-react-novu-node
Thank you for reading!