Build a polling web app with Next.js
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.
- Next.js: this is a framework for developing server-side rendered applications, just as you would with PHP, but this time with React.
- Pusher: this is a framework that allows you to build realtime applications with its easy to use pub/sub messaging API.
- 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.
- components: any Next.js component we’ll create will go here.
- css: styles for our components and pages would go here.
- 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.
``` javascript
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.
``` javascript
// 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
/* 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
``` javascript
// 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.
``` javascript
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.
``` javascript
// 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.
``` javascript
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`.
``` javascript
"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](https://github.com/Robophil/pet-wars).
13 May 2018
by Christian Nwamba