Creating a realtime data table with React and Go
You need to have Go installed and configured on your system. Some knowledge of Go is required.
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.
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
In order to follow along, you will need to create a free Pusher account. This is done by visiting the Pusher dashboard and logging in, creating a new account if needed. Next click on Channels apps on the sidebar, followed by Create Channels 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.
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:
# Mac and Linux
$ mkdir -p $GOPATH/src/pusher/running-results-table
$ cd $GOPATH/src/pusher/running-results-table
# Windows Powershell
mkdir -path $env:GOPATH/src/pusher/running-results-table
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.
package db
type Record struct {
Name string `json:"name"`
Time float32 `json:"time"`
}
func NewRecord(name string, time float32) Record {
return Record{name, time}
}
type Database struct {
contents []Record
}
func New() Database {
contents := make([]Record, 0)
return Database{contents}
}
func (database *Database) AddRecord(r Record) {
database.contents = append(database.contents, r)
}
func (database *Database) GetRecords() []Record {
return database.contents
}
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:
package webapp
import (
"net/http"
"pusher/running-results-table/internal/db"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func StartServer(database *db.Database) {
r := gin.Default()
r.Use(cors.Default())
r.GET("/results", func(c *gin.Context) {
results := database.GetRecords()
c.JSON(http.StatusOK, gin.H{
"results": results,
})
})
r.POST("/results", func(c *gin.Context) {
var json db.Record
if err := c.BindJSON(&json); err == nil {
database.AddRecord(json)
c.JSON(http.StatusCreated, json)
} else {
c.JSON(http.StatusBadRequest, gin.H{})
}
})
r.Run()
}
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:
package main
import (
"pusher/running-results-table/internal/db"
"pusher/running-results-table/internal/webapp"
)
func main() {
database := db.New()
webapp.StartServer(&database)
}
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
:
$ go run running-results-table.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /results --> pusher/running-results-table/internal/webapp.StartServer.func1 (3 handlers)
[GIN-debug] POST /results --> pusher/running-results-table/internal/webapp.StartServer.func2 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[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:
package notifier
import (
"pusher/running-results-table/internal/db"
"github.com/pusher/pusher-http-go"
)
type Notifier struct {
notifyChannel chan<- bool
}
func notifier(database *db.Database, notifyChannel <-chan bool) {
client := pusher.Client{
AppId: "PUSHER_APP_ID",
Key: "PUSHER_KEY",
Secret: "PUSHER_SECRET",
Cluster: "PUSHER_CLUSTER",
Secure: true,
}
for {
<-notifyChannel
data := map[string][]db.Record{"results": database.GetRecords()}
client.Trigger("results", "results", data)
}
}
func New(database *db.Database) Notifier {
notifyChannel := make(chan bool)
go notifier(database, notifyChannel)
return Notifier{
notifyChannel,
}
}
func (notifier *Notifier) Notify() {
notifier.notifyChannel <- true
}
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:
import (
"net/http"
"pusher/running-results-table/internal/db"
"pusher/running-results-table/internal/notifier"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
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:
r.POST("/results", func(c *gin.Context) {
var json db.Record
if err := c.BindJSON(&json); err == nil {
database.AddRecord(json)
c.JSON(http.StatusCreated, json)
notifierClient.Notify()
} else {
c.JSON(http.StatusBadRequest, gin.H{})
}
})
We now need to provide the Notifier
to the StartServer
function for it to use. Update running-results-table.go
to read as follows:
package main
import (
"pusher/running-results-table/internal/db"
"pusher/running-results-table/internal/notifier"
"pusher/running-results-table/internal/webapp"
)
func main() {
database := db.New()
notifierClient := notifier.New(&database)
webapp.StartServer(&database, ¬ifierClient)
}
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:
$ create-react-app ui
$ 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:
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div className="App">
</div>
);
}
}
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:
$ npm install --save semantic-ui-react semantic-ui-css
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.
+ semantic-ui-react@0.80.0
+ 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:
import React from 'react';
import { Table, Header, Segment, Label } from 'semantic-ui-react'
export default function ResultsTable({results}) {
const rows = results.map(((result, index) => {
let color;
if (index === 0) {
color='yellow';
} else if (index === 1) {
color='grey';
} else if (index === 2) {
color='orange';
}
return (
<Table.Row key={ index }>
<Table.Cell>
<Label ribbon color={color}>{ index + 1 }</Label>
</Table.Cell>
<Table.Cell>{ result.name }</Table.Cell>
<Table.Cell>{ result.time }</Table.Cell>
</Table.Row>
);
}));
return (
<div className="ui container">
<Segment>
<Header>Results </Header>
<Table striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Position</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Time</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{ rows }
</Table.Body>
</Table>
</Segment>
</div>
);
}
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:
import React from 'react';
import ResultsTable from './ResultsTable';
export default class ConnectedResultsTable extends React.Component {
state = {
results: []
};
componentDidMount() {
fetch('http://localhost:8080/results')
.then((response) => response.json())
.then((response) => this.setState(response));
}
render() {
return <ResultsTable results={this.state.results} />;
}
}
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:
import React, { Component } from 'react';
import ConnectedResultsTable from './ConnectedResultsTable';
class App extends Component {
render() {
return (
<div className="App">
<ConnectedResultsTable />
</div>
);
}
}
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:
import React from 'react';
import { Form, Header, Segment, Button } from 'semantic-ui-react'
export default class NewResultsForm extends React.Component {
state = {
name: '',
time: ''
};
onChangeName = this._onChangeName.bind(this);
onChangeTime = this._onChangeTime.bind(this);
onSubmit = this._onSubmit.bind(this);
render() {
return (
<div className="ui container">
<Segment vertical>
<Header>New Result</Header>
<Form onSubmit={this.onSubmit}>
<Form.Field>
<label>Name</label>
<input placeholder='Name' value={this.state.name} onChange={this.onChangeName} />
</Form.Field>
<Form.Field>
<label>Time</label>
<input placeholder='Time' value={this.state.time} onChange={this.onChangeTime} />
</Form.Field>
<Button type='submit'>Submit</Button>
</Form>
</Segment>
</div>
);
}
_onChangeName(e) {
this.setState({
name: e.target.value
});
}
_onChangeTime(e) {
this.setState({
time: e.target.value
});
}
_onSubmit() {
const payload = {
name: this.state.name,
time: parseFloat(this.state.time)
};
fetch('http://localhost:8080/results', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
this.setState({
name: '',
time: ''
});
}
}
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:
import React, { Component } from 'react';
import ConnectedResultsTable from './ConnectedResultsTable';
import NewResultsForm from './NewResultsForm';
class App extends Component {
render() {
return (
<div className="App">
<ConnectedResultsTable />
<NewResultsForm />
</div>
);
}
}
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:
import Pusher from 'pusher-js';
const socket = new Pusher('PUSHER_KEY', {
cluster: 'PUSHER_CLUSTER',
encrypted: true
});
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:
const channel = socket.subscribe('results');
channel.bind('results', (data) => {
this.setState(data);
});
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.
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?
15 May 2018
by Graham Cox