Build a polling web app with Next.js

  • Christian Nwamba
May 13th, 2018
You will need Node and npm or Yarn installed on your machine. A basic understanding of JavaScript will be helpful.

Polls are a good way of capturing popular opinions from people within a limited set of options. We'll set out on a quest to find out the most popular household pet. Dogs, cats or hamsters.

Like all polls, we'll want to see the results in realtime and we'll call this app "PET WARS: The polls awakening".

At the end of this, users should be able to cast their votes and see the results change as other users cast theirs too.

Prerequisite

Kindly ensure you have Node, Npm or Yarn installed on your machine before moving past this section. This will be needed for running and managing the dependencies needed by our application. Also, no knowledge of React is required, but a basic understanding of JavaScript may be helpful.

  1. Next.js: this is a framework for developing server-side rendered applications, just as you would with PHP, but this time with React.
  2. Pusher: this is a framework that allows you to build realtime applications with its easy to use pub/sub messaging API.
  3. Chart.js: this is a library that makes plotting charts pretty easy. More specifically, we'll be using react-chartjs2 which is a simple wrapper to make this easier to use in React.js applications.

App structure

We'll start by setting up our Next.js application. The easiest way to go about this is to clone the nextjs-javascript-starter repo. Run:

    git clone https://github.com/Robophil/nextjs-javascript-starter.git pet-wars

This will clone the starter pack into a directory called pet-wars. Our app directory will look like this.

  1. components: any Next.js component we'll create will go here.
  2. css: styles for our components and pages would go here.
  3. pages: any .js file in this directory would be served as a page. So any page we'll want to create would go here.

Install dependencies

To install the dependencies needed by our app, run:

    # enter app directory
    cd pet-wars

    # install dependencies from nextjs-javascript-starter
    yarn 
    # OR
    npm install

    # add client-side dependencies
    yarn add react-chartjs-2 chart.js axios pusher-js
    # OR
    npm install --save react-chartjs-2 chart.js axios pusher-js

    # add server-side dependencies
    yarn add cors express pusher body-parser
    # OR
    npm install -save cors express pusher body-parser

Now we have all dependencies needed by our app installed.

Getting our Pusher credentials

If you don't have a Pusher account already, kindly create one here. Once you have an account, simply head down to you dashboard and create an app. Once that is done, click on App Keys and note your credentials. We'll be needing them in a bit.

Now that we have all dependencies and credentials needed to build our application, let's get building!

Create the chart component

We'll need to display the results of the polls as they happen to users using a bar chart. Start by creating the file Chart.js in the components directory.

    // components/Chart.js
    import React from 'react'
    import { Bar } from 'react-chartjs-2'

    export default class Chart extends React.Component {
      render () {
        return (
          <Bar
            data={parseData(this.props.data)}
            width={50}
            height={100}
            options={options}
            />
        )
      }
    }

The first two lines import our dependencies. In our render method, we declare the Bar component and pass in required props.

The parseData method is responsible for passing in the styles and configuration needed by the Bar component. The data parameter it receives is an array of poll values to be displayed on the graph. Copy the code block below and paste after line 5.

    const parseData = data => ({
      labels: ['Dogs %', 'Cats %', 'Hamsters %'],
      datasets: [
        {
          label: 'The polls awakening',
          backgroundColor: [
            'rgba(255, 99, 132, 0.2)',
            'rgba(54, 162, 235, 0.2)',
            'rgba(255, 206, 86, 0.2)'
          ],
          borderColor: [
            'rgba(255, 99, 132, 0.2)',
            'rgba(54, 162, 235, 0.2)',
            'rgba(255, 206, 86, 0.2)'
          ],
          borderWidth: 5,
          hoverBackgroundColor: 'rgba(255,99,132,0.4)',
          hoverBorderColor: 'rgba(255,99,132,1)',
          data
        }
      ]
    })

    const options = {
      scales: {
        yAxes: [{
          ticks: {
            beginAtZero: true
          }
        }]
      },
      maintainAspectRatio: false
    }

Creating the voting component

Like any good poll, users need to be able to cast their votes easily. We'll be building a Vote-buttons component. When the user votes for their selected candidate, the result will be sent to the server and other users will receive updates. Start by creating the file Vote-buttons.js in the component directory.

    // components/Vote-buttons.js
    import React from 'react'
    import '../css/vote-buttons.css'

    export default class VoteButtons extends React.Component {
      render () {
        return (
          <div className={'vote-button-group'}>
            <button className={'vote-button'} onClick={() => this.props.handleVote({vote: 'dogs'})}>Vote Dogs</button>

            <button className={'vote-button'} onClick={() => this.props.handleVote({vote: 'cats'})}>Vote Cats</button>

            <button className={'vote-button'} onClick={() => this.props.handleVote({vote: 'hamsters'})}>Vote Hamsters</button>
          </div>
        )
      }
    }

This simply creates three buttons that accept an onClick event that would send the votes down to the server.

Styling the button component

We want our buttons to sit on the same line right beneath the bar chart. Simply create the file vote-buttons.css in the CSS directory and add the following:

    /* css/vote-buttons.css */
    .vote-button-group {
      display: flex;
      width: 100%;
    }

    button.vote-button {
        flex-direction: row;
        flex: 1;
        margin: 20px;
        height: 40px;
        border-radius: 5%;
    }

Creating the index page

Any .js file created in the pages directory becomes a page that can be served by Next.js. We already have a index.js file in our page directory. Replace the content with the following below

    // pages/index.js
    import React from 'react'
    import axios from 'axios'
    import Pusher from 'pusher-js'
    import Chart from '../components/Chart'
    import VoteButtons from '../components/Vote-buttons'

    var pusher = new Pusher('app_key', {
      cluster: 'cluster',
      encrypted: true
    })
    const channel = pusher.subscribe('pet-wars')

    export default class Index extends React.Component {
      constructor (props) {
        super(props)
        this.state = {
          data: [0, 0, 0]
        }
      }

      render () {
        return (
          <div>
            <Chart data={this.state.data} />
            <VoteButtons handleVote={this.handleVote.bind(this)} />
          </div>
        )
      }
    }

We start by importing our dependencies which include the components we just created. We’ll also initialize Pusher ****with our credentials and subscribe to the channel pet-wars. Replace app_key with your app key gotten from your Pusher dashboard. At the bottom of the page, we’ll render our Chart component with the VoteButtons component right below it.

Still, in our pages/index.js file, we want our application to receive changes update when voting happen elsewhere. So we subscribe our app to listen for updates once the component has mounted. The handleVote method simply submits votes made for any pet to the server.

Copy the code block below and paste on line 21.

      componentDidMount () {
        this.receiveUpdateFromPusher()
      }

      receiveUpdateFromPusher () {
        channel.bind('new-votes', data => {
          this.setState({
            data
          })
        })
        console.log('app subscription to event successful')
      }

      handleVote (data) {
        axios.post('http://localhost:8080/vote', data)
        .then(res => {
          console.log('received by server')
        })
        .catch(error => {
          throw error
        })
      }

Create our simple server

Votes made are sent to the server and dispersed to all users using Pusher. First, we need to build the route where votes will be sent to. Start by creating the file server.js in your root directory.

Here, Express is initialized with some middleware and our application started on port 8080. Pusher is also initialized with its credentials which can be gotten from the dashboard. The function getPercentage turns the cast votes to a percentage.

    // server.js
    const express = require('express')
    const app = express()
    const bodyParser = require('body-parser')
    const cors = require('cors')
    const Pusher = require('pusher')

    app.use(cors())
    app.use(bodyParser.urlencoded({ extended: true }))
    app.use(bodyParser.json())

    const port = process.env.PORT || 8080

    const pusher = new Pusher({
      appId: 'app_id',
      key: 'key',
      secret: 'secret',
      cluster: 'cluster',
      encrypted: true
    })

    let dogs = 0
    let cats = 0
    let hamsters = 0

    const getPercentage = value => (value * 100) / (dogs + cats + hamsters)

    app.listen(port, function () {
      console.log('Node app is running at localhost:' + port)
    })

Next, we create the endpoint where votes will be sent to. If the vote is for a dog, the count is increased and we update the channel pet-wars with the event new-votes. Before the values are published, the method getPercentage is called to transform the vote counts to a percentage.

Copy the code block below and paste on line 25.

    app.post('/vote', function (req, res) {
      const {vote} = req.body
      if (vote === 'dogs') {
        dogs++
      }
      if (vote === 'cats') {
        cats++
      }
      if (vote === 'hamsters') {
        hamsters++
      }
      pusher.trigger('pet-wars', 'new-votes', [getPercentage(dogs), getPercentage(cats), getPercentage(hamsters)])
      res.sendStatus(200)
    })

Test our application

For convenience, update the script field of our package.json file with the snippet below. Also update the name field, changing it's value from nextjs-javascript-starter to pet-wars.

    "scripts": {
        "client": "next",
        "server": "node server.js"
    }

This would allow us to start both our Next.js app and our api server by running the following.

    # start next.js app
    yarn run client

    # start api server
    yarn run server

Your app will be running on http://localhost:3000. Open it in as many browser tabs as possible and cast your votes. Watch it update in one tab as votes are cast in another tab.

Conclusion

We've been able to build a realtime polling app for finding out the favourite household pets between dogs, cats and hamsters. We learnt how to use Pusher to publish and subscribe to an event in a Next.js application. The complete source code can be found here.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.