Build a realtime table with Next.js

  • Christian Nwamba
May 2nd, 2018
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.

  1. Next.js: this is a framework for producing server 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. 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!!

  • 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.