Build a custom poll component with React & Node.js

In this tutorial we'll be building a React poll component that allows users to vote on a list of options and then display the results. Votes from the poll will be saved/retrieved from a JSON file using Node.js.

Here's what the completed component will look like:

Alt Text

Let's get started by setting a the project using Create React App:

npx create-react-app react-poll
cd react-poll

Setting up the Node.js backend

For the backend we’ll need to install the following modules:

npm install express cors body-parser
npm install nodemon --save-dev
  • express - used to create the endpoint for the GET & POST requests
  • cors - allows the frontend and backend to share resources.
  • body-parser - parse incoming requests so they're available under the req.body property.
  • nodemon - restarts the server when a file is saved so we don't have to do it manually.

Next inside the /src folder create the following files:

cd src
touch server.js data.json

Open the data.json file and add the following which will define the options for the poll:

    [
       { "id": 0, "votes": 0, "option": "Option One" },
       { "id": 1, "votes": 0, "option": "Option Two" },
       { "id": 2, "votes": 0, "option": "Option Three" },
       { "id": 3, "votes": 0, "option": "Option Four" }
    ]

In the server.js file let's start by loading the modules we previously installed. We're also loading the built in Node.js file system module ("fs") which is used to update the JSON file when a vote is submitted:

    const express = require("express");
    const cors = require("cors");
    const bodyParser = require("body-parser");
    const fs = require("fs");

Define a new Express server using both cors & bodyParser:

    const app = express();
    app.use(cors());
    app.use(bodyParser.json());

Complete the app setup with a app.listen() function that binds and listens for connections on port 5000:

    app.listen(5000, () => console.log("Server Running..."));

We can test the setup at this point by running the nodemon server.js command. If successful you'll see the "Server Running..." message in the terminal. With the server running we now just need to implement the code for the GET (fetch data) and POST (submit data) requests.

We'll start with the GET request which simply sends the data.json file:

    const pollData = require("./data.json");
    app.get("/poll", function (req, res) {
      res.send(pollData);
    });

For the POST request we check that the body data (req.body) was sent, if so it gets saved to the data.json file otherwise an error message is returned:

    app.post("/poll", function (req, res) {  
      if (req.body) {
        fs.writeFileSync("data.json", JSON.stringify(req.body));
        res.send({
          message: "Data Saved",
        });
      } else {
        res.status(400).send({
          message: "Error No Data",
        });
      }
    });

That completes the setup of the backend we can now move on to creating the React component.

Creating the React component

Let's start by creating a component folder and creating the JavaScript and CSS files:

mkdir components
cd components
touch Poll.js Poll.css

Start by adding the basic component structure in Poll.js. We'll be creating a functional component that makes use of the useState & useEffect hooks:

    import React, { useState, useEffect } from "react";
    import "./Poll.css";

    function Poll() {  
    }
    export default Poll;

Inside the Poll() function we'll first declare the variables for the State:

    const [voteData, setVoteData] = useState();
    const [totalVotes, setTotalVotes] = useState(0);
    const [voted, setVoted] = useState(false);
  • voteData - stores the data from the GET request.
  • totalVotes - stores the calculated total of all votes submitted.
  • voted - used to check if the user has already voted.

Using the Fetch API we make a GET request to the /poll endpoint and store the response in voteData. We also calculate the total number of votes and store them in totalVotes:

    const url = "http://localhost:5000/poll";
    useEffect(() => {
      fetch(url)
        .then((response) => response.json())
        .then((data) => {
          setVoteData(data);
          let sum = 0;
          data.forEach(function (obj) {
            sum += obj.votes;
          });
          setTotalVotes(sum);
        });
    }, []);

Next comes the submitVote function that will be triggered by an onClick event. Here we're incrementing the vote count for the selected option and then sending the updated data to the server:

    const submitVote = (e) => {
        if(voted === false) {
          const voteSelected = e.target.dataset.id;
          const voteCurrent = voteData[voteSelected].votes;
          voteData[voteSelected].votes = voteCurrent + 1;
          setTotalVotes(totalVotes + 1);
          setVoted(!voted);
          const options = {
            method: "POST",
            body: JSON.stringify(voteData),
            headers: { "Content-Type": "application/json" },
          };
          fetch(url, options)
            .then((res) => res.json())
            .then((res) => console.log(res));
        }
      };

Once the voteData has been fetched we can map each of the options into <li> elements:

    let pollOptions;
    if (voteData) {
      pollOptions = voteData.map((item) => {
        return (
          <li key={item.id}>
            <button onClick={submitVote} data-id={item.id}>
              {item.option}
              <span>- {item.votes} Votes</span>
            </button>          
          </li>
        );
      });
    }

To complete the component we need to add the return statement which includes a heading, the poll options, and also displays the total vote count:

    return (
      <div className="poll">
        <h1>Which option do you like the best?</h1>
        <ul className={voted ? "results" : "options"}>
          {pollOptions}
        </ul>
        <p>Total Votes: {totalVotes}</p>
      </div>
    );
  • {voted ? "results" : "options"} - will change the class if voted is true. This allows us to modify the styling of the component after a user has voted.

All that's required now is to load the component into the App.js as follows:

    import React from "react";
    import logo from "./logo.svg";
    import "./App.css";
    import Poll from "./components/Poll";

    function App() {
      return (
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <Poll />
          </header>
        </div>
      );
    }

    export default App;

We'll complete the tutorial by adding some CSS but at this point it's a good idea to test that everything is working by running the following command:

npm run start

When you submit a vote you should see the total votes increase, along with the vote count for the option you selected. You should also only be allowed to vote once (unless you refresh the browser).

Styling the component

We'll give the poll a fixed width and center aligns all the options:

    .poll {
      font-size: 0.9rem;
    }
    .poll ul {
      padding: 0;
      width: 300px;
      margin: auto;
    }
    .poll ul li {
      list-style: none;
      border: 2px solid #00ddf9;
      border-radius: 3px;
      line-height: 1rem;
      margin-bottom: 1rem;
      width: 100%;
    }

Remove the default <button> styles and set its width/height to fill the <li> so the onClick is triggered no matter where the user clicks within the option:

    .poll ul li button {
      border: none;
      background: transparent;
      outline: none;  
      height: 100%;
      width: 100%;
      display: block;
      padding: 15px;
      color: #fff;
      font-size: 1rem;
      font-weight: bold;
    }

We'll hide the vote count for each option and only display it after they have voted to prevent any bias. To provide a visual indicator that the vote has been submitted the border color is also altered:

    .poll ul li span {
      display: none;
    }
    .poll ul.results li button span {
      display: inline-block;
      margin-left: 8px;
    }
    .poll ul.results li {
      border-color: #999;
    }