Websocket Real Time Vote App

  1. GitHub Repository Link

  2. Tailwind + Vite + React app

» What are we building?

  1. In this, blog we will build a real-time vote app with the help of websocket(socket.io).

  2. To look at the demo, please visit the youtube video and look at the end part.

» Difference between HTTP vs. Websocket

WebSocket Vs HTTP HTTP:

HTTP is a stateless protocol, meaning each request from a client to the server is independent and unrelated to any previous request.

Each request from the client to the server is made in a new connection, and once the response is provided, the connection is closed.

HTTP follows a request-response model, where the client sends a request, and the server responds to that request.

Websocket:

WebSocket is a protocol providing bidirectional communication. It is designed to be implemented in web browsers and web servers to provide real-time communication.

As I said it is bidirectional meaning both the client and the server can send messages to each other independently at any time.

WebSocket maintains a persistent connection between the client and the server, allowing both parties to send data at any time without the need for initiating a new connection.

» Initializing Project / Server

Now that we have a basic understanding of how web socket works. Let’s create our project.

Open the terminal and create a new project folder and name whatever you want and then inside this folder we create another folder named server.

mkdir websocket-vote-app
cd websocket-vote-app
mkdir server
cd server
pnpm init
pnpm i core express socket.io nodemon

And we also initialise this as a node js / express server by running the command : pnpm init and this will generate package.json file and then back to terminal run the command: pnpm i core express socket.io nodemon.

These are all the dependencies that we need for our server.

Open the project in VS code and create an index.js file into the root of the server folder and in package.json file we add another script that is: “start”: “nodemon index.js” which will help us start the server.

"start": "nodemon index.js", 

» Initialize the Client

Now we will create our frontend / client which we will build with react and tailwind using vite and pnpm. So open another terminal in VS code run the command

pnpm create vite@latest client -- --template react
cd client

And choose React and JavaScript.

Navigate to the client folder and run the below command and it will generate our tailwind.config.js and postcss.config.js files.

pnpm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Now head over to the tailwind website(link given above) and follow the instructions.

Change tailwind.config.js with these:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Also change index.css file present in src dir of client with these:

@tailwind base;
@tailwind components;
@tailwind utilities;

Lastly let’s install the dependencies we need for our react app:

So in client folder in terminal run:

pnpm i socket.io-client flowbite-react

And thats all. We have done the initial setup for both the client and the server.

» Create the Server

Open the index.js file present in the server folder and paste the below given code.

// server/index.js
const cors = require("cors");
const express = require("express");
const { Server } = require("socket.io");

const app = express();
app.use(cors({ origin: "http://localhost:5173" }));
const server = require("http").createServer(app);
const io = new Server(server, {
  cors: {
    origin: "http://localhost:5173",
    methods: ["GET", "POST"],
  },
});

io.use(addUser);

function addUser(socket, next) {
  const user = socket.handshake.auth.token;
  if (user) {
    try {
      socket.data = { ...socket.data, user: user };
    } catch (err) {}
  }
  next();
}

const poll = {
  question: "Vote for one of the best leader?",
  options: [
    {
      id: 1,
      text: "John",
      votes: [],
    },
    {
      id: 2,
      text: "Jane",
      votes: [],
    },
    {
      id: 3,
      text: "Raj",
      votes: [],
    },
    {
      id: 4,
      text: "Gina",
      votes: [],
    },
    {
      id: 5,
      text: "Sophia",
      votes: [],
    },
    {
      id: 6,
      text: "Nina",
      votes: [],
    },
  ],
};

io.on("connection", (socket) => {
  console.log("a user connected", socket.data.user);

  socket.on("updateState", () => {
    console.log("client asked For State Update");
    socket.emit("updateState", poll);
  });

  socket.on("vote", (optionId) => {
    poll.options.forEach((option) => {
      option.votes = option.votes.filter((user) => user !== socket.data.user);
    });
    const option = poll.options.find((o) => o.id === optionId);
    if (!option) {
      return;
    }
    option.votes.push(socket.data.user);
    io.emit("updateState", poll);
  });

  socket.on("disconnect", () => {
    console.log("user is disconnected");
  });
});

server.listen(8000, () => {
  console.log("listening on Port:8000");
});

Basically this file index.js will be used with a client side that is react in our case and this going to connect through socket.io and will allow user to vote on poll.

The server will keep track of the votes and will update all the connected clients in real-time whenever a vote is casted.

And that’s all we need for the server side our code.

» Create Client side

Let’s now move onto and create our client side.

So in the src folder present in the client folder, create a file named useSocket.jsx which is a custom hook that will create a Socket.IO client and manage the connection state.

// client/src/useSocket.jsx
import { useState, useEffect } from 'react'; 
import socketIOClient from 'socket.io-client';

export function useSocket({ endpoint, token }) {
  const socket = socketIOClient(endpoint, {
    auth: {
      token: token
    }
  });
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    console.log('useSocket useEffect', endpoint, socket)

    function onConnect() {
      setIsConnected(true)
    }

    function onDisconnect() {
      setIsConnected(false)
    }

    socket.on('connect', onConnect)
    socket.on('disconnect', onDisconnect)

    return () => {
      socket.off('connect', onConnect)
      socket.off('disconnect', onDisconnect)
    }
  }, [token]);

  return {
    isConnected,
    socket,
  };
}

We import necessary modules and functions at top.

The useSocket function takes an object as a parameter with endpoint and token properties. It uses these to create a new Socket.IO client.

We uses the useState hook to manage the connection status of the socket. The isConnected state is initially set to false.

The useEffect hook is used to manage side effects. In this case, it sets up event listeners for the ‘connect’ and ‘disconnect’ events of the socket. When the socket connects, it sets isConnected to true, and when it disconnects, it sets isConnected to false. The effect runs whenever the token changes.

The return function of the useEffect hook removes the event listeners when the component using the hook unmounts or when the token changes.

The hook returns an object with the isConnected state and the socket object. This allows components that use this hook to know whether the socket is connected and to interact with the socket. And that’s what we will have for this file.

» Layout Component

  1. Now we will create a React component file(Layout.jsx) that defines two components: Navbar and Layout.

Navbar: This is a functional component that takes children as a prop and renders them inside a nav element. The nav element has Tailwind CSS classes applied to it for styling.

Layout: This is the main component in this file. It also takes children as a prop, as well as a user prop.

// client/src/Layout.jsx
import "./index.css";
import React from "react";

const Navbar = ({ children }) => (
  <nav className="flex items-center justify-between p-5">{children}</nav>
);

export const Layout = ({ children, user }) => {
  return (
    <div className="p-8">
      <Navbar>
        <a href="#" className="flex items-center">
          <span className="self-center whitespace-nowrap text-xl 
          text-gray-700 font-semibold dark:text-gray-600">
            Vote App
          </span>
        </a>
        {!!user && (
          <div className="flex flex-row items-center">
            <span className="mr-4 text-lg">{user}</span>

            <img
              alt="User settings"
              src={`https://xsgames.co/
              randomusers/avatar.php?g=female&username=${user}`}
              className="rounded-full h-10 w-10"
            />
          </div>
        )}
      </Navbar>
      <div className="grid place-items-center mt-8">{children}</div>
    </div>
  );
};

» Main Component: App

Next we want to work with the App.jsx file.

This is a React component named App that uses a custom hook useSocket to connect to a Socket.IO server and manage real-time voting for a poll.

It uses useState to manage the state of the poll and useMemo to generate a random user and calculate total votes.

It then uses the useSocket hook to establish a connection with the server and listen for updates to the poll’s state.

We also define a handleVote function to emit a ‘vote’ event to the server when a user votes.

It uses useEffect to emit an ‘updateState’ event when the component mounts.

Lastly we renders a layout with a list of options for the poll. Each option is a card that displays the option’s text, and votes. Users can vote for an option if they haven’t already. The percentage of votes for each option is visually represented by a progress bar.

// client/src/App.jsx
import { useState, useMemo, useEffect } from "react";
import { Layout } from "./Layout";
import { Button, Card } from "flowbite-react"; 
import { useSocket } from "./useSocket";

const App = () => {
  const [poll, setPoll] = useState(null);
  
  const names = ["Alice", "Bailey", "Bridget", "Kia", 
  "Sara", "Mia", "Grace", "Heidi", "Eva", "June"];

  const randomUser = useMemo(() => {
    const randomName = names[Math.floor(Math.random() 
    * names.length)];
    return `${randomName}`;
}, []);

const { socket, isConnected } = useSocket({
  endpoint: `http://localhost:8000`,
  token: randomUser,
});

const totalVotes = useMemo(() => {
  return (
    poll?.options
    .reduce((acc, option) => acc + option.votes.length, 0) ?? 0
    );
  }, [poll]);

  socket.on("updateState", (newState) => {
    setPoll(newState);
  });
  
  useEffect(() => {
    socket.emit("updateState"); 
  }, []);

  function handleVote(optionId) {
    socket.emit("vote", optionId); 
  }

  return (
    <Layout user={randomUser}>
      <div className="w-full mx-auto p-8">
        <h1 className="text-2xl font-bold text-center">
          {poll?.question ?? "Loading..."}
        </h1>
        {poll && (
          <div className="mt-6 grid 
          sm:grid-cols-1 md:grid-cols-3 gap-4">
            {poll.options.map((option) => (
              <Card
                key={option.id}
                className="relative transition-all 
                duration-300 min-h-[130px] bg-gray-200 p-2"
              >
                <div className="z-10">
                  <div className="mb-2">
                    <h2 className="text-xl 
                    font-semibold">{option.text}</h2>
                  </div>
                  <div className="absolute bottom-5 right-5">
                    {randomUser && !option.votes.includes(randomUser) ? (
                      <Button
                        className="bg-teal-500 hover:bg-teal-700 p-2 
                        text-white"
                        onClick={() => handleVote(option.id)}
                      >
                        Vote
                      </Button>
                    ) : (
                      <Button disabled className="bg-gray-700
                       text-white p-2">
                        Voted
                      </Button>
                    )}
                  </div>
                  {option.votes.length > 0 && ( 
                    <div className="mt-2 flex gap-2 
                    flex-wrap max-w-[75%]">
                      {option.votes.map((vote) => (
                        <div
                          key={vote}
                          className="py-1 px-3 bg-gray-700 
                          rounded-lg flex 
                          items-center justify-center shadow text-sm"
                        >
                          <div className="w-2 h-2
                           bg-green-500 rounded-full mr-2"></div>
                          <div className="text-gray-100">{vote}</div>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
                <div className="absolute top-5 right-5 p-2 
                text-sm font-semibold 
                bg-gray-700 text-white rounded-lg z-10">
                  {option.votes.length} / {totalVotes} 
                </div>
                <div
                  className="absolute bottom-0 inset-x-0 bg-gray-200 
                  rounded-md overflow-hidden h-4"
                >
                  <div
                    className="bg-gradient-to-r 
                    from-teal-400 to-purple-500 transition-all 
                    duration-300 h-full"
                    style={{
                      width: `${
                        totalVotes > 0
                          ? (option.votes.length / totalVotes) * 100
                          : 0
                      }%`,
                    }}
                  ></div>
                </div>
              </Card>
            ))}
          </div>
        )}
      </div>
    </Layout>
  );
};
export default App;

And that’s all for this file too.

Now we are down with both client and server.

» Start the server & client

Now let’s start the server and client. Open the terminal and run “pnpm start” in the server folder and open another window in terminal and run “pnpm run dev” to start the client.

Now let’s head to this localhost:5173 and there we have our app. Now try to vote, it increases the count. And also try doing it on several windows and look at how it updates on all the windows.


And that’s it!

👉 I hope this helped you. And I’ll see you in next blog till then bye bye.