Build a realtime table with Next.js
You need Node and npm installed on your machine. A basic knowledge of JavaScript will be helpful.
Realtime applications are generally applications that produce time sensitive data or updates that requires immediate attention or consumption. From flight management software to following up with the score line and commentary when your favorite football team is playing.
We’ll be building a realtime application that will show live updates on reviews about the next movie users want to watch at the cinema. All that juicy reviews from fans, viewers and critics around the world, and I’ll want them in real time. Let’s call it Rotten pepper.
The application will contain a form that allows users to fill in their review easily and will also display a table showing reviews left by users world wide in realtime. This part of the application will be built with Next.js
The other important part of this application is the API, where reviews posted by the user will go to. We’ll build this using Express and Node. Pusher would be the glue that sticks both ends together.
Prerequisites
We’ll be using the following tools to help build this quickly.
- Next.js: this is a framework for producing server 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.
- React Table: this is a lightweight table built for React for showing extensive data.
Please ensure you have Node and npm installed before starting the tutorial.
No knowledge of React is required, but a basic understanding of JavaScript may be helpful.
Let’s get building.
App structure
If you have no idea about Next.js, I recommend you take a look here. It’s pretty easy and in less than an hour, you’ll be able to build real applications using it.
Let’s create the directory where our app will sit:
# make directory and cd into it
mkdir movie-listing-next && cd movie-listing-next
# make pages, components and css directory
mkdir pages
mkdir components
mkdir css
Now we can go ahead to install dependencies needed by our application. I’ll be using Yarn for my dependency management, but feel free to use npm also.
Install dependencies using Yarn:
# initilize project with yarn
yarn init -y
# add dependencies with yarn
yarn add @zeit/next-css axios next pusher-js react react-dom react-table
Let’s add the following to the script
field in our package.json
and save. This makes running commands for our app more easier.
// package.json
{
"scripts": {
"dev": "next",
"server": "node server.js"
}
}
For users to submit their reviews, they’ll need a form where they can input their name, review and rating. This is a snippet from [components/form.js](https://github.com/Robophil/movie-listing-next/blob/master/components/form.js)
, which is a simple React form that takes the name
, review
and rating
. You’ll need to create yours in the components
directory.
Snippets from [components/form.js](https://github.com/Robophil/movie-listing-next/blob/master/components/form.js)
:
export default class Form extends React.Component {
....
render () {
return (
<form onSubmit={this.handleSubmit}>
<div>
<label>
Name:
<br />
<input type='text' value={this.state.name} onChange={this.handleChange.bind(this, 'name')} />
</label>
</div>
<div>
<label>
Review:
<br />
<textarea rows='4' cols='50' type='text' value={this.state.review} onChange={this.handleChange.bind(this, 'review')} />
</label>
</div>
<div>
<label>
Rating:
<br />
<input type='text' value={this.state.rating} onChange={this.handleChange.bind(this, 'rating')} />
</label>
</div>
<input type='submit' value='Submit' />
</form>
)
}
}
If you’re a React developer, you should feel right at home here. On form submission, the data is being passed down to this.props.handleFormSubmit(this.state)
. This props
is passed down from a different component as we’ll soon see.
Now we have our form, but we still need a page to list all the reviews submitted by users. The size of our reviews could grow rapidly and we still want this in realtime, so it’s best to consider pagination from the outset. That’s why we’ll be using react-table
, as highlighted above this is lightweight and will give us pagination out of the box.
The snippet below is from our index page, which you’ll need to create here [pages/index.js](https://github.com/Robophil/movie-listing-next/blob/master/pages/index.js)
.
// pages/index.js
import React from 'react'
import axios from 'axios'
import ReactTable from 'react-table'
import 'react-table/react-table.css'
import '../css/table.css'
import Form from '../components/form'
import Pusher from 'pusher-js'
Here we import our dependencies which include axios
for making http calls, our styles from table.css
and the form component we created earlier on.
// pages/index.js
const columns = [
{
Header: 'Name',
accessor: 'name'
},
{
Header: 'Review',
accessor: 'review'
},
{
Header: 'Rating',
accessor: 'rating'
}
]
const data = [
{
name: 'Stan Lee',
review: 'This movie was awesome',
rating: '9.5'
}
]
React-table, which is pretty easy to set up needs a data
and columns
props to work. There’s a pretty easy example here if you want to learn more. We’re adding a sample review to data
to have at least one review when we start our app.
// pages/index.js
const pusher = new Pusher('app-key', {
cluster: 'cluster-location',
encrypted: true
})
const channel = pusher.subscribe('rotten-pepper')
export default class Index extends React.Component {
constructor (props) {
super(props)
this.state = {
data: data
}
}
render () {
return (
<div>
<h1>Rotten <strike>tomatoes</strike> pepper</h1>
<strong>Movie: Infinity wars </strong>
<Form handleFormSubmit={this.handleFormSubmit.bind(this)} />
<ReactTable
data={this.state.data}
columns={columns}
defaultPageSize={10}
/>
</div>
)
}
}
Here, we created our React component and initialize Pusher and subscribe to the rotten-pepper
channel. Kindly get your app-id
from your Pusher dashboard and if you don’t have an account, kindly create one here. The state value this.data
is initialized with the sample data created above and our render
method renders both or form and our table.
At this point, we’re still missing a few vital parts. Pusher has been initialized, but it’s currently not pulling any new reviews and updating our table.
To fix that, add the following to your react component in pages/index.js
// pages/index.js
componentDidMount () {
this.receiveUpdateFromPusher()
}
receiveUpdateFromPusher () {
channel.bind('new-movie-review', data => {
this.setState({
data: [...this.state.data, data]
})
})
}
handleFormSubmit (data) {
axios.post('http://localhost:8080/add-review', data)
.then(res => {
console.log('received by server')
})
.catch(error => {
throw error
})
}
In componentDidMount
, we’re calling the method receiveUpdateFromPusher
which would receive new reviews submitted by users and update our table. We’re calling receiveUpdateFromPusher
in componentDidMount
so this only get called once.
The handleFormSubmit
method is responsible for sending the review submitted by users down to your endpoint. This is passed as a props to the the form component as mentioned before.
// next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS()
This should be placed in a file called next.config.js
in your root directory movie-listing-next
. It’s responsible for loading all .css
files which contains our styles on app startup.
Now that our app can load .css
properly, create the file css/form.css
which is needed by components/form.js
to style our app’s form:
form {
margin: 30px 0;
}
form div {
margin: 10px 0;
}
To keep the content of our review table center aligned, create the file css/table.css
and add the following style snippet.
.rt-td {
text-align: center;
}
To set the root structure of our app, we create pages/_document.js
. This is where the rest of our app will sit.
// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render () {
return (
<html>
<Head>
<title>Movie listing</title>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
Now, let’s setup the endpoint where all reviews submitted will be received.
Rotten pepper endpoint
This is where all the magic happens. When a review gets submitted, we’ll want other users to be aware of the new data and this is where Pusher shines. Create a file server.js
at the root of your application and add the following snippet as it’s content. Remember to visit your Pusher dashboard to get your appId
, appKey
, appSecret
.
// server.js
const pusher = new Pusher({
appId: 'appId',
key: 'appKey',
secret: 'appSecret',
cluster: 'cluster',
encrypted: true
})
app.post('/add-review', function (req, res) {
pusher.trigger('rotten-pepper', 'new-movie-review', req.body)
res.sendStatus(200)
})
From above, once the user hits /add-review
we trigger an event new-movie-review
with pusher which clients are currently listening on. We pass it the new review that was submitted and the connected clients update themselves.
The values for appId
, appSecret
and appKey
should be replaced with actual credentials. This can be gotten from your app dashboard on Pusher, and if you don’t have an account simply head down to https://pusher.com/ and create an account.
Let’s add dependencies need by our app:
# add dependencies needed by server.js
yarn add body-parser cors express pusher
At this point, the dependencies
field in our package.json
should contain the following below:
"dependencies": {
"@zeit/next-css": "^0.1.5",
"axios": "^0.18.0",
"body-parser": "^1.18.2",
"cors": "^2.8.4",
"express": "^4.16.3",
"next": "^5.1.0",
"pusher": "^1.5.1",
"pusher-js": "^4.2.2",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-table": "^6.8.2"
}
if not, simply replace the contents of the dependencies
field in your package.json
and run
# install dependencies from package.json
yarn
The entire content of server.js
is right below. The line const port = process.env.PORT || 8080
simply picks up the preferred port
to run our app and app.listen(port, function () {}
starts our app on that port
.
// 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: 'appId',
key: 'appKey',
secret: 'appSecret',
cluster: 'cluster',
encrypted: true
})
app.post('/add-review', function (req, res) {
pusher.trigger('rotten-pepper', 'new-movie-review', req.body)
res.sendStatus(200)
})
app.listen(port, function () {
console.log('Node app is running at localhost:' + port)
})
Now let’s see if what we’ve done so far works.
In one bash window:
# start next app
yarn run dev
and for our endpoint simply run in a new bash window:
# start api server
yarn run server
You can open [http://localhost:3000](http://localhost:3000)
in as many tabs as possible and see if a review posted in one tab gets to the others.
Conclusion
Building a realtime application can be super easy with the right tools. Pusher takes all that socket and connection work out of the way and allow us focus on the app we’re building.
Now I can sit back and watch reviews come :-)
The repo where this was done can be found here. Feel free to fork and improve.
Obviously this needs some more styling. How do you think we could improve this more?
Happy hacking!!
2 May 2018
by Christian Nwamba