category: How toFeb 02, 2023

Creating an upvote system with React and Socket.io 🥳 🔝

An upvote system is a way for users to express their approval or agreement with content by "voting" it up. The content with the most upvotes is typically considered the most popular or well-received by the community.

What is this article about?

Upvotes became a great way to understand what your visitors want. You can take websites like ProductHunt, and public roadmaps like GleapUpvotyProdcamp, have the ability to let user share their thoughts (in votes).
Even Reddit, one of the most popular social media lets people upvote or downvote your posts. We are going to build something similar with images!

Upvote

In this article, you’ll learn how to create an upvoting application that allows users to upload images using Websockets and upvote their favorite photos. You’ll also learn how to send emails via EmailJS to notify users when their images gain a vote.

Why Socket.io (Websockets)?

Websockets allows us to use bi-directional communication with the server. It means that if we put in an upvote, we can inform the other user about the new upvote without refreshing the page or using long-polling.

Socket.io is a popular JavaScript library that allows us to create real-time, bi-directional communication between software applications and a Node.js server. It is optimised to process a large volume of data with minimal delay and provides better functionalities, such as fallback to HTTP long-polling or automatic reconnection.

Novu – the first open-source notification infrastructure

Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in Facebook – Websockets), Emails, SMSs and so on.

I would be super happy if you could give us a star! And let me also know in the comments ❤️
https://github.com/novuhq/novu

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

Here we’ll set up the project environment for the image upvoting application. You will 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 upvote-app
2cd upvote-app
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, React Toastify, and React Router. React Router is a JavaScript library that enables us to navigate between pages in a React application, and React Toastify is used to display colourful notifications to the users.

1npm install socket.io-client react-router-dom react-toastify

Delete the redundant files such as the logo and the test files from the React app, and update the App.js file to display Hello World as below.

1function App() {
2    return (
3        <div>
4            <p>Hello World!</p>
5        </div>
6    );
7}
8export default App;

Add the Socket.io client API to the React app as below:

1import { io } from "socket.io-client";
2
3//👇🏻 http://localhost:4000 is where the server host URL.
4const socket = io.connect("http://localhost:4000");
5
6function App() {
7    return (
8        <div>
9            <p>Hello World!</p>
10        </div>
11    );
12}
13export default App;

Navigate into the server folder and create a package.json file.

1cd server & npm init -y

Install Express.js, CORS, Nodemon, and Socket.io Server API.

1npm install express cors nodemon socket.io react-icons
2

Express.js is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains.

Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.

Create an index.js file – the entry point to the web server.

1touch index.js

Set up a Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.

1//👇🏻index.js
2const express = require("express");
3const app = express();
4const PORT = 4000;
5
6app.use(express.urlencoded({ extended: true }));
7app.use(express.json());
8
9app.get("/api", (req, res) => {
10    res.json({
11        message: "Hello world",
12    });
13});
14
15app.listen(PORT, () => {
16    console.log(`Server listening on ${PORT}`);
17});

Import the HTTP and the CORS library to allow data transfer between the client and the server domains.

1const express = require("express");
2const app = express();
3const PORT = 4000;
4
5//👇🏻 New imports
6const http = require("http").Server(app);
7const cors = require("cors");
8
9app.use(express.urlencoded({ extended: true }));
10app.use(express.json());
11app.use(cors());
12
13app.get("/api", (req, res) => {
14    res.json({
15        message: "Hello world",
16    });
17});
18
19http.listen(PORT, () => {
20    console.log(`Server listening on ${PORT}`);
21});

Next, add Socket.io to the project to create a real-time connection. Before the app.get() block, copy the code below.

1//👇🏻 New imports
2.....
3const socketIO = require('socket.io')(http, {
4    cors: {
5        origin: "http://localhost:3000"
6    }
7});
8
9//👇🏻 Add this before the app.get() block
10socketIO.on('connection', (socket) => {
11    console.log(`⚡: ${socket.id} user just connected!`);
12
13    socket.on('disconnect', () => {
14      socket.disconnect()
15      console.log('🔥: A user disconnected');
16    });
17});

From the code snippet above, the socket.io("connection") function establishes a connection with the React app, then creates a unique ID for each socket and logs the ID to the console whenever a user visits the web page.

When you refresh or close the web page, the socket fires the disconnect event showing that a user has disconnected from the socket.

Configure Nodemon by adding the start command to the list of scripts in the package.json file. The code snippet below starts the server using Nodemon.

1//👇🏻 In server/package.json"
2"scripts": {
3    "test": "echo \"Error: no test specified\" && exit 1",
4    "start": "nodemon index.js"
5}

You can now run the server with Nodemon by using the command below.

1npm start

Building the user interface

Here, we’ll create the user interface for the upvoting application to enable users to sign in, upload images, and upvote any picture of their choice.
There are two rules required when building the upvoting application:

  • users can only vote once.
  • users can not upvote their own images.

Later in the tutorial, I will guide you on how you can build such a efficient upvoting system.

Navigate into the client/src folder and create a components folder containing the Login.jsRegister.jsPhotos.jsUploadPhoto.jsMyPhotos, and SharePhoto.js files.

1cd client
2mkdir components
3cd components
4touch Login.js Register.js Photos.js UploadPhoto.js MyPhoto.js SharePhoto.js

From the code snippet above:

  • The Login component is the application’s home page. It prompts users to sign in to the application.
  • The Register component enables new users to create an account before they can sign in to the application.
  • The Photos component is the home page displayed to the users after authentication. Users can view all the available images on this page and upvote them.
  • The UploadPhoto is only visible to authenticated users and allows users to upload images to the list of photos on the web application.
  • The MyPhoto page allows users to view only their uploaded images and share their profile links with friends.
  • The SharePhoto component is a dynamic route that shows all the images uploaded by a user.

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

1import React from "react";
2//👇🏻 React Router configuration & routes
3import { BrowserRouter, Routes, Route } from "react-router-dom";
4import Photos from "./components/Photos";
5import Login from "./components/Login";
6import Register from "./components/Register";
7import UploadPhoto from "./components/UploadPhoto";
8import MyPhotos from "./components/MyPhotos";
9import SharePhoto from "./components/SharePhoto";
10//👇🏻 React Toastify configuration
11import { ToastContainer } from "react-toastify";
12import "react-toastify/dist/ReactToastify.css";
13//👇🏻 Websockets configuration
14import { io } from "socket.io-client";
15
16const App = () => {
17    const socket = io.connect("http://localhost:4000");
18
19    return (
20        <>
21            <BrowserRouter>
22                <Routes>
23                    <Route path='/' element={<Login socket={socket} />} />
24                    <Route path='/register' element={<Register socket={socket} />} />
25                    <Route path='/photos' element={<Photos socket={socket} />} />
26                    <Route
27                        path='/photo/upload'
28                        element={<UploadPhoto socket={socket} />}
29                    />
30                    <Route path='/user/photos' element={<MyPhotos socket={socket} />} />
31                    <Route path='/share/:user' element={<SharePhoto socket={socket} />} />
32                </Routes>
33            </BrowserRouter>
34            <ToastContainer />
35        </>
36    );
37};
38
39export 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    margin: 0;
4    box-sizing: border-box;
5    font-family: "Space Grotesk", sans-serif;
6    color: #2b3a55;
7}
8body {
9    padding: 0;
10    margin: 0;
11}
12button {
13    border: none;
14    outline: none;
15    cursor: pointer;
16}
17input {
18    padding: 10px 15px;
19}
20.navbar {
21    width: 100%;
22    min-height: 10vh;
23    padding: 20px;
24    display: flex;
25    align-items: center;
26    justify-content: space-between;
27    background-color: #f2e5e5;
28    position: sticky;
29    top: 0;
30    z-index: 10;
31}
32.nav__BtnGroup a,
33.nav__BtnGroup button {
34    padding: 15px;
35    width: 200px;
36    font-size: 16px;
37    cursor: pointer;
38    outline: none;
39    background-color: #fff;
40    border: none;
41    border-radius: 3px;
42    text-decoration: none;
43}
44.nav__BtnGroup a {
45    margin-right: 10px;
46}
47.nav__BtnGroup a:hover,
48.nav__BtnGroup button:hover {
49    background-color: #ce7777;
50    color: #fff;
51}
52.login,
53.register {
54    width: 100%;
55    min-height: 100vh;
56    display: flex;
57    align-items: center;
58    justify-content: center;
59    flex-direction: column;
60}
61.login__form,
62.register__form {
63    width: 500px;
64    display: flex;
65    flex-direction: column;
66}
67.input {
68    padding: 12px 10px;
69    margin-bottom: 20px;
70    border-radius: 4px;
71    border: 1px solid #e8c4c4;
72}
73.loginBtn,
74.registerBtn {
75    padding: 15px;
76    font-size: 16px;
77    cursor: pointer;
78    background-color: #f2e5e5;
79    color: #2b3a55;
80    border: none;
81    outline: none;
82    border-radius: 5px;
83    margin-bottom: 20px;
84}
85.loginBtn:hover,
86.registerBtn:hover {
87    background-color: #ce7777;
88    color: #fff;
89}
90.link {
91    color: #ce7777;
92    cursor: pointer;
93    text-decoration: none;
94}
95.photoContainer {
96    width: 100%;
97    min-height: 90vh;
98    padding: 20px;
99    display: flex;
100    align-items: center;
101    justify-content: center;
102    flex-wrap: wrap;
103}
104.photoContainer > * {
105    margin: 15px;
106}
107.photo {
108    width: 300px;
109    height: 350px;
110    position: relative;
111    border-radius: 10px;
112    box-shadow: 0 2px 8px 0 rgba(99, 99, 99, 0.2);
113}
114.imageContainer {
115    width: 100%;
116    position: relative;
117    height: 100%;
118}
119.photo__image {
120    width: 100%;
121    height: 100%;
122    position: absolute;
123    object-fit: cover;
124    border-radius: 10px;
125}
126.upvoteIcon {
127    background-color: #fff;
128    padding: 10px 20px;
129    position: absolute;
130    bottom: 5px;
131    right: 5px;
132    cursor: pointer;
133    border-radius: 5px;
134    display: flex;
135    flex-direction: column;
136    align-items: center;
137}
138.upvoteIcon:hover {
139    background-color: #f2e5e5;
140}
141
142.uploadContainer {
143    width: 100%;
144    min-height: 100vh;
145    display: flex;
146    align-items: center;
147    justify-content: center;
148}
149.uploadText {
150    width: 80%;
151}
152.uploadText form {
153    display: flex;
154    flex-direction: column;
155}
156
157.uploadText > h2 {
158    margin-bottom: 20px;
159}
160.uploadBtn {
161    margin-top: 20px;
162    padding: 10px;
163    background-color: #ce7777;
164    color: #fff;
165}
166.copyDiv {
167    width: 100%;
168    display: flex;
169    align-items: center;
170    justify-content: center;
171}
172.copyContainer {
173    margin-top: 20px;
174    padding: 15px;
175    cursor: pointer;
176    background-color: #ce7777;
177    border-radius: 5px;
178}

The Login page

Copy the code below into the Login component. The application accepts the username and password from the user.

1import React, { useEffect, useState } from "react";
2import { Link } from "react-router-dom";
3import { useNavigate } from "react-router-dom";
4
5const Login = ({ socket }) => {
6    const navigate = useNavigate();
7    const [username, setUsername] = useState("");
8    const [password, setPassword] = useState("");
9
10    const handleSignIn = (e) => {
11        if (username.trim() && password.trim()) {
12            e.preventDefault();
13            console.log({ username, password });
14            setPassword("");
15            setUsername("");
16        }
17    };
18    return (
19        <div className='login'>
20            <h2 style={{ marginBottom: "30px" }}>Login</h2>
21            <form className='login__form' method='POST' onSubmit={handleSignIn}>
22                <label htmlFor='username'>Username</label>
23                <input
24                    type='text'
25                    className='input'
26                    name='username'
27                    id='username'
28                    required
29                    value={username}
30                    onChange={(e) => setUsername(e.target.value)}
31                />
32                <label htmlFor='password'>Password</label>
33                <input
34                    type='password'
35                    className='input'
36                    name='password'
37                    id='password'
38                    required
39                    minLength={6}
40                    value={password}
41                    onChange={(e) => setPassword(e.target.value)}
42                />
43                <button className='loginBtn'>LOG IN</button>
44                <p style={{ textAlign: "center" }}>
45                    Don't have an account?{" "}
46                    <Link className='link' to='/register'>
47                        Create one
48                    </Link>
49                </p>
50            </form>
51        </div>
52    );
53};
54
55export default Login;
Register Page

The Register page

Copy the code below into the Register.js file to accepts the user’s email, username and password.

1import React, { useState, useEffect } from "react";
2import { Link } from "react-router-dom";
3import { useNavigate } from "react-router-dom";
4
5const Register = ({ socket }) => {
6    const navigate = useNavigate();
7    const [username, setUsername] = useState("");
8    const [password, setPassword] = useState("");
9    const [email, setEmail] = useState("");
10
11    const handleRegister = (e) => {
12        e.preventDefault();
13        if (username.trim() && password.trim() && email.trim()) {
14            console.log({ username, email, password });
15            setPassword("");
16            setUsername("");
17            setEmail("");
18        }
19    };
20    return (
21        <div className='register'>
22            <h2 style={{ marginBottom: "30px" }}>Register</h2>
23            <form className='register__form' method='POST' onSubmit={handleRegister}>
24                <label htmlFor='email'>Email Address</label>
25                <input
26                    type='email'
27                    className='input'
28                    name='email'
29                    id='email'
30                    required
31                    value={email}
32                    onChange={(e) => setEmail(e.target.value)}
33                />
34
35                <label htmlFor='username'>Username</label>
36                <input
37                    type='text'
38                    className='input'
39                    name='username'
40                    id='username'
41                    required
42                    value={username}
43                    onChange={(e) => setUsername(e.target.value)}
44                />
45
46                <label htmlFor='password'>Password</label>
47                <input
48                    type='password'
49                    className='input'
50                    name='password'
51                    id='password'
52                    required
53                    minLength={6}
54                    value={password}
55                    onChange={(e) => setPassword(e.target.value)}
56                />
57                <button className='registerBtn'>REGISTER</button>
58                <p style={{ textAlign: "center" }}>
59                    Already have an account?{" "}
60                    <Link className='link' to='/'>
61                        Sign in
62                    </Link>
63                </p>
64            </form>
65        </div>
66    );
67};
68
69export default Register;
Register Page

The Photos component

This component is divided into two sub-components which are the navigation and the main container containing the available images.

Photo Component

Copy the code below into the Photos.js file.

1import React, { useEffect, useState } from "react";
2import Nav from "./Nav";
3import PhotoContainer from "./PhotoContainer";
4
5const Home = ({ socket }) => {
6    const [photos, setPhotos] = useState([
7        {
8            id: "1",
9            image_url:
10                "https://raw.githubusercontent.com/novuhq/blog/main/upvote-app-with-react-and-nodejs/server/images/dog1.jpg",
11            vote_count: 0,
12        },
13        {
14            id: "2",
15            image_url:
16                "https://raw.githubusercontent.com/novuhq/blog/main/upvote-app-with-react-and-nodejs/server/images/dog2.jpg",
17            vote_count: 0,
18        },
19    ]);
20
21    return (
22        <div>
23            <Nav />
24            <PhotoContainer photos={photos} socket={socket} />
25        </div>
26    );
27};
28
29export default Home;

Create the Nav and PhotoContainer sub-components.

1touch Nav.js PhotoContainer.js

Copy the code below into the Nav.js file.

1import React from "react";
2import { Link } from "react-router-dom";
3
4const Nav = () => {
5    return (
6        <nav className='navbar'>
7            <h3>PhotoShare</h3>
8            <div className='nav__BtnGroup'>
9                <Link to='/user/photos' style={{ marginRight: "10px" }}>
10                    My Photos
11                </Link>
12                <Link to='/photo/upload'>Upload Photo</Link>
13            </div>
14        </nav>
15    );
16};
17
18export default Nav;

Update the PhotoContainer.js file as below:

1import React, { useEffect } from "react";
2import { MdOutlineArrowUpward } from "react-icons/md";
3
4const PhotoContainer = ({ photos, socket }) => {
5    const handleUpvote = (id) => {
6        console.log("Upvote", id);
7    };
8
9    return (
10        <main className='photoContainer'>
11            {photos.map((photo) => (
12                <div className='photo' key={photo.id}>
13                    <div className='imageContainer'>
14                        <img
15                            src={photo.image_url}
16                            alt={photo.id}
17                            className='photo__image'
18                        />
19                    </div>
20
21                    <button className='upvoteIcon' onClick={() => handleUpvote(photo.id)}>
22                        <MdOutlineArrowUpward
23                            style={{ fontSize: "20px", marginBottom: "5px" }}
24                        />
25                        <p style={{ fontSize: "12px", color: "#ce7777" }}>
26                            {photo.vote_count}
27                        </p>
28                    </button>
29                </div>
30            ))}
31        </main>
32    );
33};
34
35export default PhotoContainer;

The UploadPhoto component

Update the UploadPhoto.js to contain the code below:

1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const UploadPhoto = ({ socket }) => {
5    const navigate = useNavigate();
6    const [photoURL, setPhotoURL] = useState("");
7
8    const handleSubmit = (e) => {
9        e.preventDefault();
10        console.log(photoURL);
11    };
12
13    return (
14        <main className='uploadContainer'>
15            <div className='uploadText'>
16                <h2>Upload Image</h2>
17                <form method='POST' onSubmit={handleSubmit}>
18                    <label>Paste the image URL</label>
19                    <input
20                        type='text'
21                        name='fileImage'
22                        value={photoURL}
23                        onChange={(e) => setPhotoURL(e.target.value)}
24                    />
25                    <button className='uploadBtn'>UPLOAD</button>
26                </form>
27            </div>
28        </main>
29    );
30};
31
32export default UploadPhoto;
MyPhotos component

The MyPhotos component

Here, you’ll learn how to copy and paste content with a click of a button using the React-copy-to-clipboard library. Within the MyPhotos component, users can copy their profile URLs and share them with others.

React-copy-to-clipboard is a package that allows us to copy and paste contents via a button click in React.

Copy to Clipboard

Install the React-copy-to-clipboard library by running the code below.

1npm install react-copy-to-clipboard

Copy the code below into the MyPhotos.js file.

1import React, { useEffect, useState } from "react";
2//👇🏻 React Router configs
3import { Link } from "react-router-dom";
4import { useNavigate } from "react-router-dom";
5import PhotoContainer from "./PhotoContainer";
6//👇🏻 React-copy-to-clipboard config
7import { CopyToClipboard } from "react-copy-to-clipboard";
8
9const MyPhotos = ({ socket }) => {
10    const navigate = useNavigate();
11    const [photos, setPhotos] = useState([]);
12    const [userLink, setUserLink] = useState("");
13
14    //👇🏻 navigates users to the homepage (for now)
15    const handleSignOut = () => {
16        localStorage.removeItem("_id");
17        localStorage.removeItem("_myEmail");
18        navigate("/");
19    };
20
21    //👇🏻 This function runs immediately the content is copied
22    const copyToClipBoard = () => alert(`Copied ✅`);
23
24    return (
25        <div>
26            <nav className='navbar'>
27                <h3>PhotoShare</h3>
28
29                <div className='nav__BtnGroup'>
30                    <Link to='/photo/upload'>Upload Photo</Link>
31                    <button onClick={handleSignOut}>Sign out</button>
32                </div>
33            </nav>
34
35            <div className='copyDiv'>
36                <CopyToClipboard
37                    text={userLink}
38                    onCopy={copyToClipBoard}
39                    className='copyContainer'
40                >
41                    <span className='shareLink'>Copy your share link</span>
42                </CopyToClipboard>
43            </div>
44
45            <PhotoContainer socket={socket} photos={photos} />
46        </div>
47    );
48};
49
50export default MyPhotos;

From the code snippet above, CopyToClipboard is a component provided by React-copy-to-clipboard that accepts two props: text– the content and the function that runs after copying the content – onCopy.

The code snippet above represents the page layout. We’ll create the functionality in the subsequent sections.

The SharePhoto component

Copy the code below into the SharePhoto.js file.

1import React, { useEffect, useState } from "react";
2import { useParams } from "react-router-dom";
3import Nav from "./Nav";
4import PhotoContainer from "./PhotoContainer";
5
6const SharePhoto = ({ socket }) => {
7    const navigate = useNavigate();
8    const [photos, setPhotos] = useState([]);
9    //👇🏻 This accepts the username from the URL (/share/:user)
10    const { user } = useParams();
11
12    return (
13        <div>
14            <Nav />
15            <PhotoContainer socket={socket} photos={photos} />
16        </div>
17    );
18};
19
20export default SharePhoto;

Congratulations!🥂 You’ve completed the user interface for the upvoting application.
For the remaining part of this article, you’ll learn how to send data between the React.js application and the Node.js server.

How to create an authentication flow with Socket.io

Here, I’ll guide you through creating an authentication flow via Socket.io. A user can create an account, and log in and out of the application.

The user registration workflow

Update the handleRegister function within the Register.js component to send the user’s email, username, and password to the server.

1const handleRegister = (e) => {
2    e.preventDefault();
3    if (username.trim() && password.trim() && email.trim()) {
4        //👇🏻 triggers a register event
5        socket.emit("register", { username, email, password });
6        setPassword("");
7        setUsername("");
8        setEmail("");
9    }
10};

Listen to the event on server as done below:

1socketIO.on("connection", (socket) => {
2    console.log(`⚡: ${socket.id} user just connected!`);
3
4    //👇🏻 Create a listener to the event
5    socket.on("register", (data) => {
6        /*
7        👇🏻 data will be an object containing the data sent from the React app
8        */
9        console.log(data);
10    });
11    socket.on("disconnect", () => {
12        socket.disconnect();
13        console.log("🔥: A user disconnected");
14    });
15});

We’ve successfully retrieved the data sent from the React app on the server. Next, let’s save the users’ details.
First, create an empty array to hold all the user details and a function that generates a random string as ID.

1//👇🏻 outside the socket.io block
2const database = [];
3const generateID = () => Math.random().toString(36).substring(2, 10);

Update the event listener to save the user’s details.

1const database = [];
2const generateID = () => Math.random().toString(36).substring(2, 10);
3
4socket.on("register", (data) => {
5    //👇🏻 Destructure the user details from the object
6    const { username, email, password } = data;
7
8    //👇🏻 Filters the database (array) to check if there is no existing user with the same email or username
9    let result = database.filter(
10        (user) => user.email === email || user.username === username
11    );
12    //👇🏻 If none, saves the data to the array. (the empty images array is required for the image uploads)
13    if (result.length === 0) {
14        database.push({
15            id: generateID(),
16            username,
17            password,
18            email,
19            images: [],
20        });
21        //👇🏻 returns an event stating that the registration was successful
22        return socket.emit("registerSuccess", "Account created successfully!");
23    }
24    //👇🏻 This runs only when there is an error/the user already exists
25    socket.emit("registerError", "User already exists");
26});

Next, listen to the two events likely to be triggered when a user registers on the web application.

1//👇🏻 Import toast from React Toastify
2import { toast } from "react-toastify";
3
4//👇🏻 Add a useEffect hook that listens to both both events
5useEffect(() => {
6    socket.on("registerSuccess", (data) => {
7        toast.success(data);
8        //👇🏻 navigates to the login page
9        navigate("/");
10    });
11    socket.on("registerError", (error) => {
12        toast.error(error);
13    });
14}, [socket, navigate]);

The Login workflow

Update the handleSignIn function within the Login.js component to send the username and password of the user to the backend server.

1const handleSignIn = (e) => {
2    if (username.trim() && password.trim()) {
3        e.preventDefault();
4        //👇🏻 triggers a login event
5        socket.emit("login", { username, password });
6        setPassword("");
7        setUsername("");
8    }
9};

Create a listener to the event on the server.

1socketIO.on("connection", (socket) => {
2    //...other functions
3
4    socket.on("login", (data) => {
5        //👇🏻 data - contains the username and password
6        console.log(data)
7    })
8}

Update the event listener to log the user into the web application as below:

1socket.on("login", (data) => {
2    //👇🏻 Destructures the credentials from the object
3    const { username, password } = data;
4
5    //👇🏻 Filters the array for existing objects with the same email and password
6
7    let result = database.filter(
8        (user) => user.username === username && user.password === password
9    );
10    //👇🏻 If there is none, it returns this error message
11    if (result.length !== 1) {
12        return socket.emit("loginError", "Incorrect credentials");
13    }
14    //👇🏻 Returns the user's email & id if the user exists
15    socket.emit("loginSuccess", {
16        message: "Login successfully",
17        data: {
18            _id: result[0].id,
19            _email: result[0].email,
20        },
21    });
22});

Listen to the two likely events within the Login.js file.

1useEffect(() => {
2    socket.on("loginSuccess", (data) => {
3        toast.success(data.message);
4        //👇🏻 Saves the user's id and email to local storage for easy identification & for making authorized requests
5        localStorage.setItem("_id", data.data._id);
6        localStorage.setItem("_myEmail", data.data._email);
7        //👇🏻 Redirects the user to the Photos component
8        navigate("/photos");
9    });
10    //👇🏻 Notifies the user of the error message
11    socket.on("loginError", (error) => {
12        toast.error(error);
13    });
14}, [socket, navigate]);

Congratulations! We’ve created the authentication flow for the application.
Lastly, let’s protect the remaining routes by allowing only authenticated users to view the pages. Copy the code snippet below into the UploadPhotoMyPhotosSharePhoto, and Photos components.

1useEffect(() => {
2    function authenticateUser() {
3        const id = localStorage.getItem("_id");
4        /*
5        👇🏻 If ID is false, redirects the user to the login page
6        */
7        if (!id) {
8            navigate("/");
9        }
10    }
11    authenticateUser();
12}, [navigate]);

Adding and displaying images with Socket.io

In this section, you’ll learn how to upload images to the server and display them within the React application via Socket.io.

Uploading images to the server via Socket.io

Navigate into the UploadPhoto component and update the handleSubmit function as below:

1const handleSubmit = (e) => {
2    e.preventDefault();
3    //👇🏻 Gets the id and email from the local storage
4    const id = localStorage.getItem("_id");
5    const email = localStorage.getItem("_myEmail");
6    /*
7    👇🏻 triggers an event to the server 
8    containing the user's credentials and the image url 
9    */
10    socket.emit("uploadPhoto", { id, email, photoURL });
11};

Create the event listener on the server that adds the image to the database.

1socket.on("uploadPhoto", (data) => {
2    //👇🏻 Gets the id, email, and image URL
3    const { id, email, photoURL } = data;
4    //👇🏻 Search the database for the user
5    let result = database.filter((user) => user.id === id);
6    //👇🏻 creates the data structure for the image
7    const newImage = {
8        id: generateID(),
9        image_url: photoURL,
10        vote_count: 0,
11        votedUsers: [],
12        _ref: email,
13    };
14    //👇🏻 adds the new image to the images array
15    result[0]?.images.unshift(newImage);
16    //👇🏻 sends a new event containing the server response
17    socket.emit("uploadPhotoMessage", "Upload Successful!");
18});

Listen for the server’s response within the React application.

1useEffect(() => {
2    socket.on("uploadPhotoMessage", (data) => {
3        //👇🏻 Displays the server's response
4        toast.success(data);
5        navigate("/photos");
6    });
7}, [socket, navigate]);

Displaying the images within the React application

Here, we’ll update the PhotosMyPhotos, and SharePhoto components to display the images.

  • The Photos component displays all the available photos within the application.
  • The MyPhotos component displays only the images uploaded by the user.
  • The SharePhoto component displays the images uploaded by a user via its username.

The Photos component

Add a useEffect hook within the Photos.js file that retrieves all the images from the server when the component mounts.

1useEffect(() => {
2    //👇🏻 search can be anything
3    socket.emit("allPhotos", "search");
4}, [socket]);

Listen to the event and return all the available images on the server.

1socket.on("allPhotos", (data) => {
2    //👇🏻 an array to contain all the images
3    let images = [];
4    //👇🏻 loop through the items in the database
5    for (let i = 0; i < database.length; i++) {
6        //👇🏻 collect the images into the array
7        images = images.concat(database[i]?.images);
8    }
9    //👇🏻 sends all the images through another event
10    socket.emit("allPhotosMessage", {
11        message: "Photos retrieved successfully",
12        photos: images,
13    });
14});

Update the useEffect hook within the Photos component to retrieve the images as below:

1useEffect(() => {
2    socket.emit("allPhotos", "search");
3    //👇🏻 retrieve all the images from the server
4    socket.on("allPhotosMessage", (data) => {
5        setPhotos(data.photos);
6    });
7}, [socket]);

The MyPhotos component

Update the useEffect hook within the MyPhotos component to trigger an event that sends the user’s id to the server via Socket.io.

1useEffect(() => {
2    function authenticateUser() {
3        const id = localStorage.getItem("_id");
4        if (!id) {
5            navigate("/");
6        } else {
7            //👇🏻 sends the user id to the server
8            socket.emit("getMyPhotos", id);
9        }
10    }
11    authenticateUser();
12}, [navigate, socket]);

Listen to the event on the server and return the user’s images.

1socket.on("getMyPhotos", (id) => {
2    //👇🏻 Filter the database items
3    let result = database.filter((db) => db.id === id);
4    //👇🏻 Returns the images and the username
5    socket.emit("getMyPhotosMessage", {
6        data: result[0]?.images,
7        username: result[0]?.username,
8    });
9});

Retrieve the images and username from the server as done below:

1useEffect(() => {
2    socket.on("getMyPhotosMessage", (data) => {
3        //👇🏻 sets the user's images
4        setPhotos(data.data);
5        //👇🏻 sets the user's profile link
6        setUserLink(`http://localhost:3000/share/${data.username}`);
7    });
8}, [socket]);

The SharePhoto component

Update the useEffect hook within the SharePhoto component to request for the user’s images when the component mounts.

1useEffect(() => {
2    function authenticateUser() {
3        const id = localStorage.getItem("_id");
4        if (!id) {
5            navigate("/");
6        } else {
7            //👇🏻 user - is the username from the profile link
8            socket.emit("sharePhoto", user);
9        }
10    }
11    authenticateUser();
12}, [socket, navigate, user]);

Listen to the event on the server and return the user’s images.

1socket.on("sharePhoto", (name) => {
2    //👇🏻 Filters the database via the username
3    let result = database.filter((db) => db.username === name);
4    //👇🏻 Returns the images via another event
5    socket.emit("sharePhotoMessage", result[0]?.images);
6});

Retrieve the images from the server as below:

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

Congratulations on making it thus far! 😊 You’ve learnt how to manipulate the data within the database and retrieve the items for each route. In the upcoming section, I’ll guide you through how to upvote the images.

How to upvote the images

Here, I will guide you through upvoting the images within the web application. Recall that users can not upvote their photos and can only upvote once.

Update the handleUpvote function within the PhotoContainer.js to send the user and the image ID to the server.

1const handleUpvote = (id) => {
2    socket.emit("photoUpvote", {
3        userID: localStorage.getItem("_id"),
4        photoID: id,
5    });
6};

Listen to the event on the server and upvote the selected image via its ID.

1socket.on("photoUpvote", (data) => 
2    const { userID, photoID } = data;
3    let images = [];
4    //👇🏻 saves all the images not belonging to the user into the images array
5    for (let i = 0; i < database.length; i++) {
6        //👇🏻 ensures that only other users' images are separated into the images array
7        if (!(database[i].id === userID)) {
8            images = images.concat(database[i]?.images);
9        }
10    }
11    //👇🏻 Filter the images array for the image selected for upvote
12    const item = images.filter((image) => image.id === photoID);
13    /*
14    👇🏻 Returns this error if the selected image doesn't belong to other users
15    */
16    if (item.length < 1) {
17        return socket.emit("upvoteError", {
18            error_message: "You cannot upvote your photos",
19        });
20    }
21    //👇🏻 Gets the list of voted users from the selected image
22    const voters = item[0]?.votedUsers;
23    //👇🏻 Checks if the user has not upvoted the image before
24    const authenticateUpvote = voters.filter((voter) => voter === userID);
25    //👇🏻 If true (the first time the user is upvoting the image)
26    if (!authenticateUpvote.length) {
27        //👇🏻 increases the vote count
28        item[0].vote_count += 1;
29        //👇🏻 adds the user ID to the list of voters
30        voters.push(userID);
31        //👇🏻 triggers this event to reflect the change in vote count
32        socket.emit("allPhotosMessage", {
33            message: "Photos retrieved successfully",
34            photos: images,
35        });
36        //👇🏻 Returns the upvote response
37        return socket.emit("upvoteSuccess", {
38            message: "Upvote successful",
39            item,
40        });
41    }
42    /*
43    👇🏻 nullifies duplicate votes. (if the user ID already exists in the array of voted users)
44    */
45    socket.emit("upvoteError", {
46        error_message: "Duplicate votes are not allowed",
47    });

Listen to the likely response from the server within the PhotoContainer.js file.

1useEffect(() => {
2    socket.on("upvoteSuccess", (data) => {
3        toast.success(data.message);
4        //👇🏻 logs the email of the user who owns the image.
5        console.log(data.item[0]._ref);
6    });
7    socket.on("upvoteError", (data) => {
8        toast.error(data.error_message);
9    });
10}, [socket]);

From the code snippet above, data.item[0]._ref is required because we want to send email notifications to the users when they upvote their images.

How to send emails via EmailJS in React

EmailJS is a JavaScript library that enables us to send emails via client-side technologies only – without a server. With EmailJS, you can send texts and email templates and add attachments to the emails.

Here, I’ll guide you through adding EmailJS to the React.js application and how to send emails to users whenever their images gain a vote.

Install EmailJS to the React application by running the code below:

1npm install @emailjs/browser

Create an EmailJS account here and add an email service provider to your account.

Create an email template as done in the image below:

EmailJS

Update the PhotoContainer.js file to send the email template to users whenever their images gain a vote.

1import emailjs from "@emailjs/browser";
2
3const PhotoContainer = ({ photos, socket }) => {
4    const handleUpvote = (id) => {
5        socket.emit("photoUpvote", {
6            userID: localStorage.getItem("_id"),
7            photoID: id,
8        });
9    };
10
11    //👇🏻 The function sends email to the user - (to_email key)
12    const sendEmail = (email) => {
13        emailjs
14            .send(
15                "YOUR_SERVICE_ID",
16                "YOUR_TEMPLATE_ID",
17                {
18                    to_email: email,
19                    from_email: localStorage.getItem("_myEmail"),
20                },
21                "YOUR_PUBLIC_KEY"
22            )
23            .then(
24                (result) => {
25                    console.log(result.text);
26                },
27                (error) => {
28                    console.log(error.text);
29                }
30            );
31    };
32
33    useEffect(() => {
34        socket.on("upvoteSuccess", (data) => {
35            toast.success(data.message);
36            //👇🏻 Pass the image owner email into the function
37            sendEmail(data.item[0]._ref);
38        });
39        socket.on("upvoteError", (data) => {
40            toast.error(data.error_message);
41        });
42    }, [socket]);
43    return <div>...</div>;
44};

You can get your EmailJS Public key from the Account section of your EmailJS dashboard.

Congratulations! You’ve completed the project for this tutorial.

Conclusion

So far, you’ve learnt the following:

  • how to set up Socket.io in a React and Node.js application,
  • create an authentication with Socket.io and React,
  • communicate between a server and a client via Socket.io, and
  • send emails with EmailJS.

This tutorial walks you through a project you can build using Socket.io and React. Feel free to improve the application by adding an authentication library and a real-time database.

The source code for this tutorial is available here:

https://github.com/novuhq/blog/tree/main/upvote-app-with-react-and-nodejs

Thank you for reading!

Help me out!

If you feel like this article helped you understand WebSockets better! I would be super happy if you could give us a star! And let me also know in the comments ❤️
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