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.
Upvotes became a great way to understand what your visitors want. You can take websites likeĀ ProductHunt, and public roadmaps likeĀ Gleap,Ā Upvoty,Ā Prodcamp, 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!
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.
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.
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
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.
mkdir upvote-appcd upvote-appmkdir client server
Navigate into the client folder via your terminal and create a new React.js project.
cd clientnpx create-react-app ./
Install Socket.io client API, 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.
Add the Socket.io client API to the React app as below:
import { io } from "socket.io-client";//\ud83d\udc47\ud83c\udffb http://localhost:4000 is where the server host URL.const socket = io.connect("http://localhost:4000");function App() { return ( Hello World!</p> </div> );}export default App;
Navigate into the server folder and create aĀ package.jsonĀ file.
cd server & npm init -y
Install Express.js, CORS, Nodemon, and Socket.io Server API.
npm install express cors nodemon socket.io react-icons
Express.jsĀ is a fast, minimalist framework that provides several features for building web applications in Node.js.Ā CORSĀ is a Node.js package that allows communication between different domains.
NodemonĀ is a Node.js tool that automatically restarts the server after detecting file changes, andĀ Socket.ioĀ allows us to configure a real-time connection on the server.
Create an index.js file ā the entry point to the web server.
touch index.js
Set up a Node.js server using Express.js. The code snippet below returns a JSON object when you visit theĀ http://localhost:4000/apiĀ in your browser.
Import the HTTP and the CORS library to allow data transfer between the client and the server domains.
const express = require("express");const app = express();const PORT = 4000;//\ud83d\udc47\ud83c\udffb New importsconst http = require("http").Server(app);const cors = require("cors");app.use(express.urlencoded({ extended: true }));app.use(express.json());app.use(cors());app.get("/api", (req, res) => { res.json({ message: "Hello world", });});http.listen(PORT, () => { console.log(`Server listening on ${PORT}`);});
Next, add Socket.io to the project to create a real-time connection. Before theĀ app.get()Ā block, copy the code below.
//\ud83d\udc47\ud83c\udffb New imports.....const socketIO = require('socket.io')(http, { cors: { origin: "http://localhost:3000" }});//\ud83d\udc47\ud83c\udffb Add this before the app.get() blocksocketIO.on('connection', (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on('disconnect', () => { socket.disconnect() console.log('\ud83d\udd25: A user disconnected'); });});
From the code snippet above, theĀ socket.io("connection")Ā function establishes a connection with the React app, then creates a unique ID for each socket and logs the ID to the console whenever a user visits the web page.
When you refresh or close the web page, the socket fires the disconnect event showing that a user has disconnected from the socket.
Configure Nodemon by adding the start command to the list of scripts in theĀ package.jsonĀ file. The code snippet below starts the server using Nodemon.
//\ud83d\udc47\ud83c\udffb In server/package.json""scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon index.js"}
You can now run the server with Nodemon by using the command below.
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.js,Ā Register.js,Ā Photos.js,Ā UploadPhoto.js,Ā MyPhotos, andĀ SharePhoto.jsĀ files.
cd clientmkdir componentscd componentstouch 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:
import React from "react";//\ud83d\udc47\ud83c\udffb React Router configuration & routesimport { BrowserRouter, Routes, Route } from "react-router-dom";import Photos from "./components/Photos";import Login from "./components/Login";import Register from "./components/Register";import UploadPhoto from "./components/UploadPhoto";import MyPhotos from "./components/MyPhotos";import SharePhoto from "./components/SharePhoto";//\ud83d\udc47\ud83c\udffb React Toastify configurationimport { ToastContainer } from "react-toastify";import "react-toastify/dist/ReactToastify.css";//\ud83d\udc47\ud83c\udffb Websockets configurationimport { io } from "socket.io-client";const App = () => { const socket = io.connect("http://localhost:4000"); return ( <> } /> } /> } /> } /> } /> } /> </Routes> </BrowserRouter> </> );};export default App;
Navigate into theĀ src/index.cssĀ file and copy the code below. It contains all the CSS required for styling this project.
Here, 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.
import React, { useEffect, useState } from "react";//\ud83d\udc47\ud83c\udffb React Router configsimport { Link } from "react-router-dom";import { useNavigate } from "react-router-dom";import PhotoContainer from "./PhotoContainer";//\ud83d\udc47\ud83c\udffb React-copy-to-clipboard configimport { CopyToClipboard } from "react-copy-to-clipboard";const MyPhotos = ({ socket }) => { const navigate = useNavigate(); const [photos, setPhotos] = useState([]); const [userLink, setUserLink] = useState(""); //\ud83d\udc47\ud83c\udffb navigates users to the homepage (for now) const handleSignOut = () => { localStorage.removeItem("_id"); localStorage.removeItem("_myEmail"); navigate("/"); }; //\ud83d\udc47\ud83c\udffb This function runs immediately the content is copied const copyToClipBoard = () => alert(`Copied \u2705`); return ( PhotoShare</h3> Upload Photo</Link> Sign out</button> </div> </nav> Copy your share link</span> </CopyToClipboard> </div> </div> );};export 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.
Copy the code below into theĀ SharePhoto.jsĀ file.
import React, { useEffect, useState } from "react";import { useParams } from "react-router-dom";import Nav from "./Nav";import PhotoContainer from "./PhotoContainer";const SharePhoto = ({ socket }) => { const navigate = useNavigate(); const [photos, setPhotos] = useState([]); //\ud83d\udc47\ud83c\udffb This accepts the username from the URL (/share/:user) const { user } = useParams(); return ( </div> );};export 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.
socketIO.on("connection", (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); //\ud83d\udc47\ud83c\udffb Create a listener to the event socket.on("register", (data) => { /* \ud83d\udc47\ud83c\udffb data will be an object containing the data sent from the React app */ console.log(data); }); socket.on("disconnect", () => { socket.disconnect(); console.log("\ud83d\udd25: A user disconnected"); });});
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.
Update the event listener to save the userās details.
const database = [];const generateID = () => Math.random().toString(36).substring(2, 10);socket.on("register", (data) => { //\ud83d\udc47\ud83c\udffb Destructure the user details from the object const { username, email, password } = data; //\ud83d\udc47\ud83c\udffb Filters the database (array) to check if there is no existing user with the same email or username let result = database.filter( (user) => user.email === email || user.username === username ); //\ud83d\udc47\ud83c\udffb If none, saves the data to the array. (the empty images array is required for the image uploads) if (result.length === 0) { database.push({ id: generateID(), username, password, email, images: [], }); //\ud83d\udc47\ud83c\udffb returns an event stating that the registration was successful return socket.emit("registerSuccess", "Account created successfully!"); } //\ud83d\udc47\ud83c\udffb This runs only when there is an error/the user already exists socket.emit("registerError", "User already exists");});
Next, listen to the two events likely to be triggered when a user registers on the web application.
//\ud83d\udc47\ud83c\udffb Import toast from React Toastifyimport { toast } from "react-toastify";//\ud83d\udc47\ud83c\udffb Add a useEffect hook that listens to both both eventsuseEffect(() => { socket.on("registerSuccess", (data) => { toast.success(data); //\ud83d\udc47\ud83c\udffb navigates to the login page navigate("/"); }); socket.on("registerError", (error) => { toast.error(error); });}, [socket, navigate]);
socketIO.on("connection", (socket) => { //...other functions socket.on("login", (data) => { //\ud83d\udc47\ud83c\udffb data - contains the username and password console.log(data) })}
Update the event listener to log the user into the web application as below:
socket.on("login", (data) => { //\ud83d\udc47\ud83c\udffb Destructures the credentials from the object const { username, password } = data; //\ud83d\udc47\ud83c\udffb Filters the array for existing objects with the same email and password let result = database.filter( (user) => user.username === username && user.password === password ); //\ud83d\udc47\ud83c\udffb If there is none, it returns this error message if (result.length !== 1) { return socket.emit("loginError", "Incorrect credentials"); } //\ud83d\udc47\ud83c\udffb Returns the user's email & id if the user exists socket.emit("loginSuccess", { message: "Login successfully", data: { _id: result[0].id, _email: result[0].email, }, });});
Listen to the two likely events within theĀ Login.jsĀ file.
useEffect(() => { socket.on("loginSuccess", (data) => { toast.success(data.message); //\ud83d\udc47\ud83c\udffb Saves the user's id and email to local storage for easy identification & for making authorized requests localStorage.setItem("_id", data.data._id); localStorage.setItem("_myEmail", data.data._email); //\ud83d\udc47\ud83c\udffb Redirects the user to the Photos component navigate("/photos"); }); //\ud83d\udc47\ud83c\udffb Notifies the user of the error message socket.on("loginError", (error) => { toast.error(error); });}, [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Ā UploadPhoto,Ā MyPhotos,Ā SharePhoto, andĀ PhotosĀ components.
useEffect(() => { function authenticateUser() { const id = localStorage.getItem("_id"); /* \ud83d\udc47\ud83c\udffb If ID is false, redirects the user to the login page */ if (!id) { navigate("/"); } } authenticateUser();}, [navigate]);
Navigate into theĀ UploadPhotoĀ component and update theĀ handleSubmitĀ function as below:
const handleSubmit = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb Gets the id and email from the local storage const id = localStorage.getItem("_id"); const email = localStorage.getItem("_myEmail"); /* \ud83d\udc47\ud83c\udffb triggers an event to the server containing the user's credentials and the image url */ socket.emit("uploadPhoto", { id, email, photoURL });};
Create the event listener on the server that adds the image to the database.
socket.on("uploadPhoto", (data) => { //\ud83d\udc47\ud83c\udffb Gets the id, email, and image URL const { id, email, photoURL } = data; //\ud83d\udc47\ud83c\udffb Search the database for the user let result = database.filter((user) => user.id === id); //\ud83d\udc47\ud83c\udffb creates the data structure for the image const newImage = { id: generateID(), image_url: photoURL, vote_count: 0, votedUsers: [], _ref: email, }; //\ud83d\udc47\ud83c\udffb adds the new image to the images array result[0]?.images.unshift(newImage); //\ud83d\udc47\ud83c\udffb sends a new event containing the server response socket.emit("uploadPhotoMessage", "Upload Successful!");});
Listen for the serverās response within the React application.
Here, weāll update theĀ Photos,Ā MyPhotos, 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.
useEffect(() => { //\ud83d\udc47\ud83c\udffb search can be anything socket.emit("allPhotos", "search");}, [socket]);
Listen to the event and return all the available images on the server.
socket.on("allPhotos", (data) => { //\ud83d\udc47\ud83c\udffb an array to contain all the images let images = []; //\ud83d\udc47\ud83c\udffb loop through the items in the database for (let i = 0; i < database.length; i++) { //\ud83d\udc47\ud83c\udffb collect the images into the array images = images.concat(database[i]?.images); } //\ud83d\udc47\ud83c\udffb sends all the images through another event socket.emit("allPhotosMessage", { message: "Photos retrieved successfully", photos: images, });});
Update the useEffect hook within theĀ PhotosĀ component to retrieve the images as below:
useEffect(() => { socket.emit("allPhotos", "search"); //\ud83d\udc47\ud83c\udffb retrieve all the images from the server socket.on("allPhotosMessage", (data) => { setPhotos(data.photos); });}, [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.
useEffect(() => { function authenticateUser() { const id = localStorage.getItem("_id"); if (!id) { navigate("/"); } else { //\ud83d\udc47\ud83c\udffb sends the user id to the server socket.emit("getMyPhotos", id); } } authenticateUser();}, [navigate, socket]);
Listen to the event on the server and return the userās images.
socket.on("getMyPhotos", (id) => { //\ud83d\udc47\ud83c\udffb Filter the database items let result = database.filter((db) => db.id === id); //\ud83d\udc47\ud83c\udffb Returns the images and the username socket.emit("getMyPhotosMessage", { data: result[0]?.images, username: result[0]?.username, });});
Retrieve the images and username from the server as done below:
useEffect(() => { socket.on("getMyPhotosMessage", (data) => { //\ud83d\udc47\ud83c\udffb sets the user's images setPhotos(data.data); //\ud83d\udc47\ud83c\udffb sets the user's profile link setUserLink(`http://localhost:3000/share/${data.username}`); });}, [socket]);
The SharePhoto component
Update theĀ useEffectĀ hook within theĀ SharePhotoĀ component to request for the userās images when the component mounts.
useEffect(() => { function authenticateUser() { const id = localStorage.getItem("_id"); if (!id) { navigate("/"); } else { //\ud83d\udc47\ud83c\udffb user - is the username from the profile link socket.emit("sharePhoto", user); } } authenticateUser();}, [socket, navigate, user]);
Listen to the event on the server and return the userās images.
socket.on("sharePhoto", (name) => { //\ud83d\udc47\ud83c\udffb Filters the database via the username let result = database.filter((db) => db.username === name); //\ud83d\udc47\ud83c\udffb Returns the images via another event socket.emit("sharePhotoMessage", result[0]?.images);});
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.
Listen to the event on the server and upvote the selected image via its ID.
socket.on("photoUpvote", (data) => const { userID, photoID } = data; let images = []; //\ud83d\udc47\ud83c\udffb saves all the images not belonging to the user into the images array for (let i = 0; i < database.length; i++) { //\ud83d\udc47\ud83c\udffb ensures that only other users' images are separated into the images array if (!(database[i].id === userID)) { images = images.concat(database[i]?.images); } } //\ud83d\udc47\ud83c\udffb Filter the images array for the image selected for upvote const item = images.filter((image) => image.id === photoID); /* \ud83d\udc47\ud83c\udffb Returns this error if the selected image doesn't belong to other users */ if (item.length < 1) { return socket.emit("upvoteError", { error_message: "You cannot upvote your photos", }); } //\ud83d\udc47\ud83c\udffb Gets the list of voted users from the selected image const voters = item[0]?.votedUsers; //\ud83d\udc47\ud83c\udffb Checks if the user has not upvoted the image before const authenticateUpvote = voters.filter((voter) => voter === userID); //\ud83d\udc47\ud83c\udffb If true (the first time the user is upvoting the image) if (!authenticateUpvote.length) { //\ud83d\udc47\ud83c\udffb increases the vote count item[0].vote_count += 1; //\ud83d\udc47\ud83c\udffb adds the user ID to the list of voters voters.push(userID); //\ud83d\udc47\ud83c\udffb triggers this event to reflect the change in vote count socket.emit("allPhotosMessage", { message: "Photos retrieved successfully", photos: images, }); //\ud83d\udc47\ud83c\udffb Returns the upvote response return socket.emit("upvoteSuccess", { message: "Upvote successful", item, }); } /* \ud83d\udc47\ud83c\udffb nullifies duplicate votes. (if the user ID already exists in the array of voted users) */ socket.emit("upvoteError", { error_message: "Duplicate votes are not allowed", });
Listen to the likely response from the server within theĀ PhotoContainer.jsĀ file.
useEffect(() => { socket.on("upvoteSuccess", (data) => { toast.success(data.message); //\ud83d\udc47\ud83c\udffb logs the email of the user who owns the image. console.log(data.item[0]._ref); }); socket.on("upvoteError", (data) => { toast.error(data.error_message); });}, [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.
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:
npm 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:
Update theĀ PhotoContainer.jsĀ file to send the email template to users whenever their images gain a vote.
import emailjs from "@emailjs/browser";const PhotoContainer = ({ photos, socket }) => { const handleUpvote = (id) => { socket.emit("photoUpvote", { userID: localStorage.getItem("_id"), photoID: id, }); }; //\ud83d\udc47\ud83c\udffb The function sends email to the user - (to_email key) const sendEmail = (email) => { emailjs .send( "YOUR_SERVICE_ID", "YOUR_TEMPLATE_ID", { to_email: email, from_email: localStorage.getItem("_myEmail"), }, "YOUR_PUBLIC_KEY" ) .then( (result) => { console.log(result.text); }, (error) => { console.log(error.text); } ); }; useEffect(() => { socket.on("upvoteSuccess", (data) => { toast.success(data.message); //\ud83d\udc47\ud83c\udffb Pass the image owner email into the function sendEmail(data.item[0]._ref); }); socket.on("upvoteError", (data) => { toast.error(data.error_message); }); }, [socket]); return ...</div>;};
You can get your EmailJS Public key from the Account section of your EmailJS dashboard.
Congratulations! Youāve completed the project for this tutorial.
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:
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