Creating a realtime data table with React and Go

Introduction

Introduction

In this article we are going to build a simple web application for storing and displaying live race results. For example, from the Olympics 100m. We are going to use the Go language for our backend and the React framework to build our web frontend. We are then going to use Pusher Channels to give live updates to all the users currently viewing the table, allowing them to see finishers in real time.

data-table-react-go-demo

Prerequisites

This article focuses on using Go and React. As such, it is important that you have Go already installed and configured on your system - including having the GOPATH set up correctly. If you do not know how to do this then the Go documentation can explain this all. A certain level of understanding of Go is assumed to follow along with this article. The “A Tour of Go” tutorial is a fantastic introduction if you are new to the language.

We are also going to use the dep tool to manage the dependencies of our backend application, so make sure that this is correctly installed as well.

Finally, in order to develop and run our web UI you will need to have a recent version of Node.js installed and correctly set up. A certain level of understanding of JavaScript is also assumed to follow along with this article.

Create a Pusher account

To get started with Pusher Channels, create a free sandbox Pusher account or sign in and go to the dashboard. Next click on Channels apps on the sidebar, followed by Create Channels app.

data-table-react-go-create-pusher-app

Fill out this dialog as needed and then click the Create my app button. Then click on App Keys and note down the credentials for later.

data-table-react-go-app-keys

Building the backend service

We are going to write our backend service using the Go language, using the library to power our HTTP service.

Our service is going to offer two endpoints:

  • GET /results - this returns the current list of results.
  • POST /results - this creates a new result to add to the list.

To start with, we need to create an area to work with. Create a new directory under your GOPATH in which to work:

1# Mac and Linux
2    $ mkdir -p $GOPATH/src/pusher/running-results-table
3    $ cd $GOPATH/src/pusher/running-results-table
4    
5    # Windows Powershell
6    mkdir -path $env:GOPATH/src/pusher/running-results-table
7    cd $env:GOPATH/src/pusher/running-results-table

We can then initialise our work area for this project. This is done using the dep tool:

    $ dep init

Doing this will create the **Gopkg.toml and Gopkg.lock files used to track our dependencies, and the vendor **directory which is used to store vendored dependencies.

The next thing to do is to create our data store. We are going to do this entirely in memory for this article, but in reality you would use a real database, for example PostgreSQL or MongoDB.

Create a new directory called internal/db under our work area, and create a db.go file in here as follows:

NOTE: the use of internal here is a convention that indicates that this is internal to our project and not to be imported by any other projects.

1package db
2    type Record struct {
3        Name string  `json:"name"`
4        Time float32 `json:"time"`
5    }
6    func NewRecord(name string, time float32) Record {
7        return Record{name, time}
8    }
9    type Database struct {
10        contents []Record
11    }
12    func New() Database {
13        contents := make([]Record, 0)
14        return Database{contents}
15    }
16    func (database *Database) AddRecord(r Record) {
17        database.contents = append(database.contents, r)
18    }
19    func (database *Database) GetRecords() []Record {
20        return database.contents
21    }

Here we are creating a new type called Record that represents the data that we store, and a new struct called Database that represents the actual database we are using. We then create some methods on the Database type to add a record and to get the list of all records.

Next we can create our web server. For this we are going to create a new directory called internal/webapp under our work area, and a new file called webapp.go in this directory as follows:

1package webapp
2    import (
3        "net/http"
4        "pusher/running-results-table/internal/db"
5        "github.com/gin-contrib/cors"
6        "github.com/gin-gonic/gin"
7    )
8    func StartServer(database *db.Database) {
9        r := gin.Default()
10        r.Use(cors.Default())
11        r.GET("/results", func(c *gin.Context) {
12            results := database.GetRecords()
13            c.JSON(http.StatusOK, gin.H{
14                "results": results,
15            })
16        })
17        r.POST("/results", func(c *gin.Context) {
18            var json db.Record
19            if err := c.BindJSON(&json); err == nil {
20                database.AddRecord(json)
21                c.JSON(http.StatusCreated, json)
22            } else {
23                c.JSON(http.StatusBadRequest, gin.H{})
24            }
25        })
26        r.Run()
27    }

This creates a function called StartServer that will create and run our web server, defining two routes on it to do the processing that we need.

We are also importing some packages that aren’t currently available - github.com/gin-gonic/gin and github.com/gin-contrib/cors. The first of these is the Gin web server itself, and the second is the contrib library to enable CORS, so that our webapp can access the backend server.

We can now use dep to ensure that this is available for us, by executing dep ensure from our top level. This will download the necessary packages and put them into our vendor directory ready to be used:

    $ dep ensure

Finally, we create a main program that actually makes use of this all. For this, in the top of the work area we create a file called running-results-table.go as follows:

1package main
2    
3    import (
4            "pusher/running-results-table/internal/db"
5            "pusher/running-results-table/internal/webapp"
6    )
7    
8    func main() {
9            database := db.New()
10    
11            webapp.StartServer(&database)
12    }

This makes use of our db and webapp modules that we’ve just written, and starts everything up correctly.

We can now run our application by executing go run running-results-table.go:

1$ go run running-results-table.go
2    [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
3     - using env:        export GIN_MODE=release
4     - using code:        gin.SetMode(gin.ReleaseMode)
5    
6    [GIN-debug] GET    /results                  --> pusher/running-results-table/internal/webapp.StartServer.func1 (3 handlers)
7    [GIN-debug] POST   /results                  --> pusher/running-results-table/internal/webapp.StartServer.func2 (3 handlers)
8    [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
9    [GIN-debug] Listening and serving HTTP on :8080

Alternatively, we can build an executable using go build running-results-table.go. This executable can then be distributed however we need to do so - for example, copying it into a Docker container or directly onto our production VMs.

Sending live updates when data changes

At this point, we can correctly create new records and retrieve all of the records that have been created. However, there is no support for live updates at this point - the client would need to keep re-requesting the data to see if anything changes.

As a better solution to this, we are going to use Pusher Channels to automatically emit events whenever a new record is created, so that all listening clients can automatically update themselves without needing to poll the server. Additionally, we are going to use Go channels to isolate the sending of Pusher events from the actual HTTP request - allowing our server to respond to the client faster, whilst still sending the event a fraction of a second later.

Create a new directory called internal/notifier under our work area, and in this create a file called notifier.go as follows:

1package notifier
2    import (
3        "pusher/running-results-table/internal/db"
4        "github.com/pusher/pusher-http-go"
5    )
6    type Notifier struct {
7        notifyChannel chan<- bool
8    }
9    func notifier(database *db.Database, notifyChannel <-chan bool) {
10        client := pusher.Client{
11            AppId:   "PUSHER_APP_ID",
12            Key:     "PUSHER_KEY",
13            Secret:  "PUSHER_SECRET",
14            Cluster: "PUSHER_CLUSTER",
15            Secure:  true,
16        }
17        for {
18            <-notifyChannel
19            data := map[string][]db.Record{"results": database.GetRecords()}
20            client.Trigger("results", "results", data)
21        }
22    }
23    func New(database *db.Database) Notifier {
24        notifyChannel := make(chan bool)
25        go notifier(database, notifyChannel)
26        return Notifier{
27            notifyChannel,
28        }
29    }
30    func (notifier *Notifier) Notify() {
31        notifier.notifyChannel <- true
32    }

NOTE: remember to update the values PUSHER_APP_ID, PUSHER_KEY, PUSHER_SECRET and PUSHER_CLUSTER to the real values you got when registering your Pusher Channels application.

There is quite a lot going on here, so lets work through it.

The first thing we do is define a new type called Notifier. This is our interface that we expose to the rest of the code through which we can notify clients of new results.

Next, we define a non-exported function called notifier that is given a reference to the database and a Go channel. This function will create our Pusher client, and then start an infinite loop of reading from the channel (which blocks until a new message comes in), retrieving the latest list of results from the database and sending them off to Pusher. We deliberately get the latest list ourselves here in case there was some delay in processing the message - this way we’re guaranteed not to miss anything.

We then create a new method called New that will return a new Notifier. Importantly in here we also start a new go-routine that runs our notifier function, which essentially means that there is a new thread of execution running that function.

Finally we have a Notify method on our Notifier that does nothing more than push a new value down our Go channel.

The end result of this is that, whenever someone calls Notifier.Notify(), we will trigger our go-routine - on a separate thread - to retrieve the current results from the database and send them to Pusher.

We now need to use dep to again ensure that this is available for us, by executing dep ensure from our top level.

    $ dep ensure

Now we want to actually make use of it. To do this, we want to update our StartServer method in internal/webapp/webapp.go to also take a new parameter notifierClient *notifier.Notifier. The new signature should be:

    func StartServer(database *db.Database, notifierClient *notifier.Notifier) {

We’ll also need to update the imports at the top to include the notifier package, as follows:

1import (
2            "net/http"
3            "pusher/running-results-table/internal/db"
4            "pusher/running-results-table/internal/notifier"
5    
6            "github.com/gin-contrib/cors"
7            "github.com/gin-gonic/gin"
8    )

Then, we want to update the handling in our POST route to call notifierClient.Notify() immediately after (or before, it makes little difference) the call to return the HTTP Status to the caller. This means that the whole route looks like:

1r.POST("/results", func(c *gin.Context) {
2            var json db.Record
3            if err := c.BindJSON(&json); err == nil {
4                database.AddRecord(json)
5                c.JSON(http.StatusCreated, json)
6                notifierClient.Notify()
7            } else {
8                c.JSON(http.StatusBadRequest, gin.H{})
9            }
10        })

We now need to provide the Notifier to the StartServer function for it to use. Update running-results-table.go to read as follows:

1package main
2    import (
3            "pusher/running-results-table/internal/db"
4            "pusher/running-results-table/internal/notifier"
5            "pusher/running-results-table/internal/webapp"
6    )
7    func main() {
8            database := db.New()
9            notifierClient := notifier.New(&database)
10            webapp.StartServer(&database, &notifierClient)
11    }

At this point, you can start up the server, call the endpoint by hand (using something like cURL or Postman), and then watch the messages appear in your Pusher Channels dashboard.

Building the web application

Now that we have our backend service, we want a UI to make use of it. This will be built using the Create React App tool and styled using Semantic UI.

To start with, we’ll create our new UI project. If create-react-app isn’t installed already then do so:

    $ npm install -g create-react-app

Then we can use it to set up the UI project:

1$ create-react-app ui
2    $ cd ui

Next we want to remove some details that we just don’t care about. These are the default UI components that come with the created application. For this, delete the files src/App.css, src/App.test.js, src/index.css and src/logo.svg.

Now replace src/App.js with the following:

1import React, { Component } from 'react';
2    class App extends Component {
3      render() {
4        return (
5          <div className="App">
6          </div>
7        );
8      }
9    }
10    export default App;

And remove the following line from src/index.js:

    import './index.css';

Now we want to add in Semantic UI to our build. This is simply done by adding the packages and including the CSS into our main file. Add the packages as follows:

1$ npm install --save semantic-ui-react semantic-ui-css
2    npm WARN ajv-keywords@3.2.0 requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself.
3    
4    + semantic-ui-react@0.80.0
5    + semantic-ui-css@2.3.1added 7 packages in 9.377s

Then add the following line back in to src/index.js:

    import 'semantic-ui-css/semantic.min.css';

Creating our data table

Next we want to create the data table to render. For this, we want to create a new file called src/ResultsTable.js as follows:

1import React from 'react';
2    import { Table, Header, Segment, Label } from 'semantic-ui-react'
3    export default function ResultsTable({results}) {
4        const rows = results.map(((result, index) => {
5            let color;
6            if (index === 0) {
7                color='yellow';
8            } else if (index === 1) {
9                color='grey';
10            } else if (index === 2) {
11                color='orange';
12            }
13            return (
14                <Table.Row key={ index }>
15                    <Table.Cell>
16                        <Label ribbon color={color}>{ index + 1 }</Label>
17                    </Table.Cell>
18                    <Table.Cell>{ result.name }</Table.Cell>
19                    <Table.Cell>{ result.time }</Table.Cell>
20                </Table.Row>
21            );
22        }));
23        return (
24            <div className="ui container">
25                <Segment>
26                    <Header>Results </Header>
27                    <Table striped>
28                        <Table.Header>
29                            <Table.Row>
30                                <Table.HeaderCell>Position</Table.HeaderCell>
31                                <Table.HeaderCell>Name</Table.HeaderCell>
32                                <Table.HeaderCell>Time</Table.HeaderCell>
33                            </Table.Row>
34                        </Table.Header>
35                        <Table.Body>
36                            { rows }
37                        </Table.Body>
38                    </Table>
39                </Segment>
40            </div>
41        );
42    }

Now we need to be able to get the actual data to render. For this we will create a new src/ConnectedResultsTable.js file that manages the state of our component, does all of the API interactions, and then renders our table with the results. This looks as follows:

1import React from 'react';
2    import ResultsTable from './ResultsTable';
3    export default class ConnectedResultsTable extends React.Component {
4        state = {
5            results: []
6        };
7        componentDidMount() {
8            fetch('http://localhost:8080/results')
9                .then((response) => response.json())
10                .then((response) => this.setState(response));
11        }
12        render() {
13            return <ResultsTable results={this.state.results} />;
14        }
15    }

This simply uses the Fetch API to retrieve the results when the component is first mounted, and then renders whatever results are currently stored in the state. This means that we will only see new results by re-rendering the page, but we’ll fix that later.

NOTE: the component uses a hard-coded URL of “http://localhost:8080”. This is where our local development server is running, but you’ll need to change this for production.

Finally, we want to actually render the table. This is done by updating the src/App.js file to look as follows:

1import React, { Component } from 'react';
2    import ConnectedResultsTable from './ConnectedResultsTable';
3    class App extends Component {
4      render() {
5        return (
6          <div className="App">
7            <ConnectedResultsTable />
8          </div>
9        );
10      }
11    }
12    export default App;

Adding new data

In order to add new data to the table, we’re going to add a simple form below our table that submits a new record to our backend. For this, we will create a new file called src/NewResultsForm.js as follows:

1import React from 'react';
2    import { Form, Header, Segment, Button } from 'semantic-ui-react'
3    export default class NewResultsForm extends React.Component {
4        state = {
5            name: '',
6            time: ''
7        };
8        onChangeName = this._onChangeName.bind(this);
9        onChangeTime = this._onChangeTime.bind(this);
10        onSubmit = this._onSubmit.bind(this);
11        render() {
12            return (
13                <div className="ui container">
14                    <Segment vertical>
15                        <Header>New Result</Header>
16                        <Form onSubmit={this.onSubmit}>
17                            <Form.Field>
18                                <label>Name</label>
19                                <input placeholder='Name' value={this.state.name} onChange={this.onChangeName} />
20                            </Form.Field>
21                            <Form.Field>
22                                <label>Time</label>
23                                <input placeholder='Time' value={this.state.time} onChange={this.onChangeTime} />
24                            </Form.Field>
25                            <Button type='submit'>Submit</Button>
26                        </Form>
27                    </Segment>
28                </div>
29            );
30        }
31        _onChangeName(e) {
32            this.setState({
33                name: e.target.value
34            });
35        }
36        _onChangeTime(e) {
37            this.setState({
38                time: e.target.value
39            });
40        }
41        _onSubmit() {
42            const payload = {
43                name: this.state.name,
44                time: parseFloat(this.state.time)
45            };
46            fetch('http://localhost:8080/results', {
47                method: 'POST',
48                headers: {
49                    'Content-Type': 'application/json'
50                },
51                body: JSON.stringify(payload)
52            });
53            this.setState({
54                name: '',
55                time: ''
56            });
57        }
58    }

NOTE: This assumes that the values entered are legal. It does not do any validation. If you enter a time that is not a number then you will not get the results you expected.

Next add this in to the src/App.js file as well. Update the file to look as follows:

1import React, { Component } from 'react';
2    import ConnectedResultsTable from './ConnectedResultsTable';
3    import NewResultsForm from './NewResultsForm';
4    class App extends Component {
5      render() {
6        return (
7          <div className="App">
8            <ConnectedResultsTable />
9            <NewResultsForm />
10          </div>
11        );
12      }
13    }
14    export default App;

Receiving live updates from Pusher

Now that we’ve got our data table, we want to make it update in real time. We will make use of the official pusher-js module for this interaction. Install this as follows:

    $ npm install --save pusher-js

We then add in the Pusher client to our src/ConnectedResultsTable.js file. Firstly add the following to the top of the file:

1import Pusher from 'pusher-js';
2    const socket = new Pusher('PUSHER_KEY', {
3        cluster: 'PUSHER_CLUSTER',
4        encrypted: true
5    });

NOTE: remember to update the values PUSHER_KEY and PUSHER_CLUSTER to the real values you got when registering your Pusher Channels application.

Then add the following in to the componentDidMount method:

1const channel = socket.subscribe('results');
2            channel.bind('results', (data) => {
3                this.setState(data);
4            });

This will automatically update our state based on receiving the data from Pusher, which in turn will automatically cause our table to re-render with the new data.

Ensure that the backend is running, by executing go run running-results-table.go as before, then start the front end by:

    $ npm start

And our application is ready to go.

data-table-react-go-demo

Conclusion

This article shows how we can easily incorporate Pusher Channels into a Go web application to give realtime updates to our clients.

All of the source code from this article is available on GitHub. Why not try extending it to support more results tables, or more types of event?