Important Update
The Guide Feature will be discontinued after December 15th, 2023. Until then, you can continue to access and refer to the existing guides.
Author avatar

Esteban Herrera

Building a Real-time Application with React, React Router, Horizon.io, and OAuth

Esteban Herrera

  • Dec 15, 2018
  • 40 Min read
  • 15,724 Views
  • Dec 15, 2018
  • 40 Min read
  • 15,724 Views
Interesting APIs

Introduction

In this tutorial, we're going to build an application that shows how to integrate React, React Router, and Horizon.io with OAuth authentication from beginning to end.

The application is simple. It just stores and presents messages in real-time:

Demo App

The entire source code is available on Github. Big thanks to Bartek Kus who help me update the code to Horizon 2.0.

Requirements

Horizon.io is a real-time backend for Javascript apps built on top of RethinkDB. If you don't know this NoSQL database, here's a tutorial that shows how it works.

So first, you'll need to install RethinkDB. There are packages for all major operative systems, here are the instructions.

Horizon is a Node.js application, so you'll also need version 4.4 or higher of Node.js and NPM installed. You can download an installer for your platform here.

Then, install Horizon (globally) by executing:

1npm install -g horizon
bash

We'll be using Horizon's version 2.0.0, if you need/want to specify this version, execute this command instead:

1npm install -g [email protected]
bash

About React, you don't need to be a guru to follow this tutorial, but you'll need to have some basic knowledge about components and how this library works.

Now that we have all we need, let's get started.

Creating an Horizon app

First, execute the following command:

1hz init react-horizon
bash

This will create a Horizon application in the directory react-horizon with the following structure:

1.hz
2 |- config.toml
3 |- schema.toml
4 |- secret.toml
5dist
6 |- index.html
7src
8.gitignore

.hz/config.toml is the TOML main configuration file for the Horizon server.

.hz/schema.toml is optionally used for the database schema and permissions.

.hz/secret.toml specifies authentication information and token secrets.

You can know more about these configuration files here.

dist is the directory where the public and static files will be stored.

src is the directory where the client side code will be stored.

.gitignore contains the following:

1rethinkdb_data
2**/*.log
3.hz/secrets.toml

For authentication, Horizon requires us to work with https, so we need to hava a SSL certificate. Luckily, Horizon comes with a tool to create a self-signed certificates (you just need to have OpenSSL installed), so cd into this directory

1cd react-horizon
bash

To keep things organized, let's create a directory to store all the configuration files of our app and cd into it:

1mkdir config && cd config
bash

Now create a directory to store the certificates:

1mkdir tls && cd tls
bash

And execute the command:

1hz create-cert
bash

This will create a horizon-cert.pem and a horizon-key.pem. By default, Horizon will look for these files in the root directory of the application when starting a secure server. If you want to change the name or the location of these files (as in our case), uncomment and change the following section of the .hz/config.toml file:

1###############################################################################
2# HTTPS Options
3# 'secure' will disable HTTPS and use HTTP instead when set to 'false'
4# 'key_file' and 'cert_file' are required for serving HTTPS
5#------------------------------------------------------------------------------
6# secure = true
7# key_file = "horizon-key.pem"
8# cert_file = "horizon-cert.pem"

To:

1###############################################################################
2# HTTPS Options
3# 'secure' will disable HTTPS and use HTTP instead when set to 'false'
4# 'key_file' and 'cert_file' are required for serving HTTPS
5#------------------------------------------------------------------------------
6secure = true
7key_file = "config/tls/horizon-key.pem"
8cert_file = "config/tls/horizon-cert.pem"

Now, the command to start the server in a development environment is:

1hz serve --dev
bash

The --dev option will set the following flags:

  • --start-rethinkdb that will start a RethinkDB server automatically
  • --secure no that will start an insecure server (with no SSL)
  • --permissions no that will disable the permissions system
  • ----auto-create-collection and --auto-create-index that will create tables and indexes if they don't exist
  • --server-static ./dist that will configure dist as the directory from which the static content will be served

However, since we are going to use HTTPS, we need to redefine the secure option, so go to the root directory of the app (cd ../..) and start the server with the following command:

1hz serve --dev --secure yes
bash

The output of this command should be similar to the following:

1App available at https://127.0.0.1:8181
2RethinkDB
3   ├── Admin interface: http://localhost:46398
4   └── Drivers can connect to port 35109
5Starting Horizon...
6🌄 Horizon ready for connections

If you go to https://localhost:8181 you should see this (after accepting the warning of the self-signed certificate):

Horizon initial app

Moreover, a RethinkDB server will be started automatically and a rethinkdb-data directory will be created. When you go to http://localhost:46398 (or whatever address Horizon gives you in the console), and then to the Tables section, you should see the following:

RethinkDB dashboard

As you can see, Horizon has created a database with the name of the project and stores metadata about collections, users, and groups. Also, it will store the data used by collections in the application.

Setting up React

We're going to use ECMAScript 2015 so let's set up Babel to transform this syntax to one most browsers can understand by creating its configuration file:

1echo '{ "presets": ["react", "es2015", "stage-0"] }' > .babelrc

Babel 6.x does not ship with any transformations enabled, so you need to explicitly tell it what transformations to run by using a preset.

The first two are very self-descriptive. The stage-x presets are changes to the language that haven’t been approved to be part of a release of Javascript.

The TC39 categorizes proposals into 4 stages:

  • stage-0 - Strawman: just an idea, possible Babel plugin.
  • stage-1 - Proposal: this is worth working on.
  • stage-2 - Draft: initial spec.
  • stage-3 - Candidate: complete spec and initial browser implementations.
  • stage-4 - Finished: will be added to the next yearly release.

stage-0 includes all plugins from presets of all levels. stage-1 includes all plugins from presets 1 to 4 and so on.

To execute Babel and bundle our scripts with their dependencies, we'll use Webpack and npm. Let's install Webpack (globally) with:

1npm install -g webpack
bash

And add a package.json configuration file with:

1npm init
bash

Or if you want to accept all the defaults:

1npm init -y
bash

Next, install the Babel dependencies and presets (and a polyfill to emulate a full ES2015 environment) with:

1npm install --save-dev babel-core babel-loader
2npm install --save-dev babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-polyfill
bash

Do the same with Webpack and React:

1npm install --save-dev webpack
2npm install --save react react-dom react-router
bash

At the time of this writing, the following are the versions saved to the package.json file:

1{
2  ...
3  "devDependencies": {
4    "babel-core": "^6.13.2",
5    "babel-loader": "^6.2.5",
6    "babel-polyfill": "^6.13.0",
7    "babel-preset-es2015": "^6.13.2",
8    "babel-preset-react": "^6.11.1",
9    "babel-preset-stage-0": "^6.5.0",
10    "webpack": "^1.13.2"
11  },
12  "dependencies": {
13    "react": "^15.3.1",
14    "react-dom": "^15.3.1",
15    "react-router": "^2.7.0"
16  }
17}
json

Now, create a webpack.config.js file at root level with the following content:

1var path = require('path');
2
3module.exports = {
4    entry: ["./src/app.js"],
5
6    output: {
7        filename: "dist/js/bundle.js",
8        sourceMapFilename: "dist/js/bundle.map"
9    },
10
11    devtool: '#source-map',
12
13    module: {
14        loaders: [
15            {
16                loader: 'babel',
17                exclude: /node_modules/
18            }
19        ]
20    }
21}
js

This way, Webpack will create a dist/js/bundle.js file with all the JavaScript code of the application (from the script /src/app.js).

Finally, let's add to package.json the following start script that packs our application and starts the Horizon server:

1{
2  ...
3  "scripts": {
4    "start": "webpack && hz serve --dev --secure yes"
5  },
6  ...
7}
json

Creating the app with React and React Router

Let's start by defining the HTML file that will contain our React application, dist/index.html:

1<!doctype html>
2<html>
3  <head>
4    <meta charset="UTF-8">
5    <title>React.js + Horizon.io</title>
6    <link rel=stylesheet href=/css/style.css />
7  </head>
8  <body>
9    <h1>React.js + Horizon.io</h1>
10   <div id="root"></div>
11   <script src="/js/bundle.js"></script>
12  </body>
13</html>
html

And the CSS to style it, dist/css/style.css:

1body {
2  background: #300637;
3  padding: 4em;
4  text-align: center;
5  font-family: "Arial, Helvetica, sans-serif";
6  color: #c0c0c0;
7}
8
9h1 {
10  font-weight: 100;
11}
12
13.menu {
14  margin-top: 3em;
15  margin-bottom: 5em;
16}
17
18.menu-option {
19  color: #999;
20  background: rgba(0, 0, 0, 0.5);
21  padding: 1em 5em;
22  font-size: 0.8em;
23  text-decoration: none;
24  letter-spacing: 2px;
25  text-transform: uppercase;
26}
27
28.menu-option:hover {
29  background: rgba(0, 0, 0, 0.4);
30  color: #fff;
31}
32
33.menu-option.active {
34  border: none;
35  background: rgba(0, 0, 0, 0.4);
36  background: #fff;
37  padding: 1.5em 5em;
38  color: #000;
39  font-size: 0.9em;
40}
41
42.login-btn {
43  border: none;
44  color: #999;
45  background: rgba(0, 0, 0, 0.5);
46  padding: 1.5em 5em;
47  font-size: 0.9em;
48  text-transform: uppercase;
49  cursor: pointer;
50}
51
52.message-input {
53  display: block;
54  margin: 0.5em;
55  padding: 0.8em;
56  width: 90%;
57  border: none;
58  font-family: sans-serif;
59  font-size: 0.9em;
60}
61
62.message-container {
63  margin: 2em auto;
64  padding: 0.8em;
65  width: 80%;
66}
67
68.message-btn {
69  padding: 0.75rem;
70  margin: 0.5em auto 1.5em;
71  width: 6em;
72  text-align: center;
73  cursor: pointer;
74}
75
76.message-row {
77  padding: 0.75em;
78  margin: 0.75em;
79  box-shadow: 0 0 1px 1px #999;
80}
css

src will store the JavaScript code of our application. The starting file will be app.js:

1import React from "react";
2import ReactDOM from "react-dom";
3
4import Router from "./router";
5
6ReactDOM.render(Router, document.getElementById("root"));
jsx

This way, router.js will contain all the routes of the application:

1import React from "react";
2import { Router, Route, browserHistory, IndexRoute } from "react-router";
3
4// Layout
5import MainLayout from "./components/main-layout";
6
7// Pages
8import Home from "./components/home";
9import MessageContainer from "./components/message-container";
10import Login from "./components/login";
11
12export default (
13  <Router history={browserHistory}>
14    <Route path="/" component={MainLayout}>
15      <IndexRoute component={Home} />
16      <Route path="messages" component={MessageContainer} />
17      <Route path="login" component={Login} />
18    </Route>
19  </Router>
20);
jsx

We'll have three nested routes (so MainLayout can serve as a template):

  • / is our home
  • /login is our login page
  • /messages is the page with the message functionality

Now let's create the components that represent these pages in the components directory.

components/main-layout.js contains the general layout of the app:

1import React, { Component } from "react";
2import { Link } from "react-router";
3import Menu from "./menu";
4
5export default class MainLayout extends Component {
6  render() {
7    return (
8      <div className="app">
9        <div>
10          <Menu />
11        </div>
12        <div>{this.props.children}</div>
13      </div>
14    );
15  }
16}
jsx

The menu is delegated to the components/menu.js component:

1import React, { Component } from "react";
2import { Link, IndexLink } from "react-router";
3
4export default class Menu extends Component {
5  render() {
6    var menu = (
7      <div className={"menu"}>
8        <IndexLink to="/" className={"menu-option"} activeClassName="active">
9          Home
10        </IndexLink>
11        <Link to="/messages" className={"menu-option"} activeClassName="active">
12          Messages
13        </Link>
14        <Link to="/login" className={"menu-option"} activeClassName="active">
15          Login
16        </Link>
17      </div>
18    );
19
20    return menu;
21  }
22}
jsx

An <IndexLink> is like a <Link>, but it's only active when the current route is exactly the linked route (otherwise, since all of these are nested routes, / and /messages will be marked as active at the same time).

For now, components/login.js will just contain a button to login with Github:

1import React, { Component } from "react";
2
3export default class Login extends Component {
4  render() {
5    return (
6      <div>
7        <button className={"login-btn"}>Login with Github</button>
8      </div>
9    );
10  }
11}
jsx

components/home.js will contain:

1import React, { Component } from "react";
2
3export default class Home extends Component {
4  render() {
5    return <h1>Home</h1>;
6  }
7}
jsx

While components/message-container.js will contain:

1import React, { Component } from "react";
2
3export default class MessageContainer extends Component {
4  render() {
5    return <h1>Message Container</h1>;
6  }
7}
jsx

If you run the application at this point with:

1npm start
bash

It should look like this:

React Checkpoint

Adding real-time functionality with Horizon

Now let's use Horizon to add real-time functionality to the application.

First, add the Horizon client dependency to package.json:

1npm install --save @horizon/client
bash

You can force the installation of version 2.0.0 with:

1npm install --save @horizon/[email protected]
bash

Then, we are going to create a container for the Horizon object, src/horizon-container.js. This will allow us to use Horizon in any part of the application with the same settings, and wrap functions to use them easily. However, right now, it will contain just:

1import Horizon from "@horizon/client";
2
3const _horizon = Horizon();
4
5export default {
6  get: () => _horizon
7};
javascript

The Horizon() object accepts some optional arguments that you can see in this page. Here we're just using the defaults.

In components/message-container.js, we're going to create a Horizon collection to hold the messages. A collection represents a table in RethinkDB, and it lets you store, retrieve, and filter documents. In this page you can know about its API.

Here's the code:

1import React, { Component } from "react";
2import Messages from "./messages";
3import Horizon from "../horizon-container";
4
5const _horizon = Horizon.get();
6const _messageCollection = _horizon("messages");
7
8export default class MessageContainer extends Component {
9  constructor(props) {
10    super(props);
11    this.state = { text: "", errorDescription: "" };
12  }
13
14  handleChangeText = e => {
15    this.setState({ text: e.target.value });
16  };
17
18  handleSubmit = () => {
19    if (this.state.text === "" || this.state.author === "") {
20      this.setState({ errorDescription: "The message is required" });
21    } else {
22      this.storeMessage("", this.state.text);
23    }
24  };
25
26  storeMessage = (user, text) => {
27    const message = {
28      text: text,
29      author: user
30    };
31    _messageCollection.store(message);
32    this.setState({ text: "", errorDescription: "" });
33  };
34
35  render() {
36    return (
37      <div>
38        <div className={"message-container"}>
39          <div>{this.state.errorDescription}</div>
40          <input
41            onChange={this.handleChangeText}
42            className={"message-input"}
43            type="text"
44            placeholder="Enter your message"
45            value={this.state.text}
46          />
47          <button className={"message-btn"} onClick={this.handleSubmit}>
48            Submit
49          </button>
50          <Messages messages={_messageCollection} />
51        </div>
52      </div>
53    );
54  }
55}
jsx

Notice how we create the message collection:

1const _messageCollection = _horizon("messages");
javascript

And how a message is inserted:

1_messageCollection.store(message);
javascript

The complete collection API is here.

In the next section we'll add the authenticated user ID. For now, we are just passing an empty string.

Also, notice that the component binds the method using arrow functions (since they capture the correct value of this in the actual context) instead of using a regular function and bind(this):

1<button className={"message-btn"} onClick={this.handleSubmit.bind(this)}>
2  Submit
3</button>
jsx

Then, the message collection is passed to a Messages component (components/messages.js):

1import React, { Component } from "react";
2import Message from "./message";
3
4export default class Messages extends Component {
5  constructor(props) {
6    super(props);
7    this.messageCollection = this.props.messages;
8    this.state = { messages: [] };
9  }
10
11  componentDidMount() {
12    this.messageCollection.watch().subscribe(
13      collection => {
14        if (collection) {
15          this.setState({ messages: collection });
16        }
17      },
18      err => {
19        console.log(err);
20      }
21    );
22  }
23
24  render() {
25    const messagesMapped = this.state.messages.map((result, index) => {
26      return <Message message={result} key={index} />;
27    });
28
29    return <div>{messagesMapped}</div>;
30  }
31}
jsx

The watch() method allows us to listen for changes in the collection in real-time. It returns an object that receives the entire collection of documents, even when a single one changes, which fits perfectly with the way React works.

This way, everything a change is detected, the state of the component changes, and this is re-rendered.

The final piece is the Message component (components/message.js), which just prints a single message:

1import React, { Component } from "react";
2
3export default class Message extends Component {
4  constructor(props) {
5    super(props);
6  }
7
8  render() {
9    const { text, author } = this.props.message;
10    return (
11      <div className={"message-row"}>
12        <div>
13          <b>Author:</b> {author}
14        </div>
15        <div>
16          <b>Text:</b> {text}
17        </div>
18      </div>
19    );
20  }
21}
jsx

If we run the application at this point, it should look like this:

Horizon Checkpoint

And in the console, the following messages will be shown (once you've entered to the Messages page):

1warn: Auto-creating collection (dev mode): messages
2warn: Collection created (dev mode): "messages"

If you go to the RethinkDB web interface (remember to look for the URL when you start the server), you'll notice the table for the collection (in this case, messages):

RethinkDB collection table

If you query this table, you'll see the stored message(s):

RethinkDB collection query

Adding Horizon OAuth authentication

Let's add authentication to our app by using Horizon's support for OAuth. For simplicity, we're going to use Github only, but the steps and configuration are almost the same for other providers (like Google or Twitter).

We'll need a client ID and a client secret. Go to https://github.com/settings/applications/new to register an application and enter the following information:

  • Application name: react_horizon (any name will do)
  • Homepage URL: https://localhost:8181 (or whatever your URL is, just remember the https part)

Registering a new OAuth application on Github

When you register the application, the client ID and client secret will be presented:

Client ID and Client secret screen

Then, you have to configure these values in the .hz/secrets.toml file:

1token_secret = "NnvpIpep8g9msem6pQHap6g38/wZ0GYQH9/NtXnUTRWlSHT28UtrbAHxxJhi+7673koIJx2Ay5kFX+zHua3fjQ=="
2
3###############################################################################
4# RethinkDB Options
5# 'rdb_user' is the user account to log in with when connecting to RethinkDB
6# 'rdb_password' is the password for the user account specified by 'rdb_user'
7#------------------------------------------------------------------------------
8# rdb_user = 'admin'
9# rdb_password = ''
10
11# [auth.auth0]
12# host = "0000.00.auth0.com"
13# id = "0000000000000000000000000"
14# secret = "00000000000000000000000000000000000000000000000000"
15# redirect_url = ""
16#
17# [auth.facebook]
18# id = "000000000000000"
19# secret = "00000000000000000000000000000000"
20#
21# [auth.google]
22# id = "00000000000-00000000000000000000000000000000.apps.googleusercontent.com"
23# secret = "000000000000000000000000"
24#
25# [auth.twitter]
26# id = "0000000000000000000000000"
27# secret = "00000000000000000000000000000000000000000000000000"
28#
29# [auth.github]
30# id = "00000000000000000000"
31# secret = "0000000000000000000000000000000000000000"
32#
33# [auth.twitch]
34# id = "0000000000000000000000000000000"
35# secret = "0000000000000000000000000000000"
36#
37# [auth.slack]
38# id = "0000000000000000000000000000000"
39# secret = "0000000000000000000000000000000"

Horizon uses JSON Web Tokens (JWTS) for user authentication. When the .hz/secrets.toml file is created, a token_secret is generated to sign JWTS, so just replace the client ID and the client secret and uncomment the Github section:

1[auth.github]
2id = "ea89158619f776d0703c"
3secret = "df0ab9c5bff3aae9209ac08241cc7fbdd4ab4144"

Now, let's modify the file src/horizon-container.js to support authentication. First, configure Horizon to support authentication with JWTS:

1const _horizon = Horizon({authType: 'token'});
js

Then, add the method clearAuthTokens() to delete the token on logout and a method to get the current user. The code should look like this:

1import Horizon from "@horizon/client";
2
3const _horizon = Horizon({ authType: "token" });
4
5export default {
6  get: () => _horizon,
7  clearAuthTokens: () => Horizon.clearAuthTokens(),
8  getCurrentUser: callback => {
9    _horizon
10      .currentUser()
11      .fetch()
12      .subscribe(user => callback(user));
13  }
14};
js

There is more than one way to implement authentication in a React/React Router application. The one that we'll use in this tutorial is high-order components.

A high-order component is a function that takes another component and returns another one that wraps it. In our case, a high-order component will be used to wrap the route to protect by checking if the user is authenticated before rendering it.

This will be the job of src/authenticate-route.js. Here's the code:

1import React, { Component, PropTypes } from "react";
2import Login from "./components/login";
3import Horizon from "./horizon-container";
4
5const _horizon = Horizon.get();
6
7export default ChildComponent => {
8  class AuthenticatedComponent extends Component {
9    constructor(props) {
10      super(props);
11      this.state = { currentUser: "" };
12    }
13
14    componentDidMount() {
15      if (_horizon.hasAuthToken()) {
16        Horizon.getCurrentUser(user => {
17          this.setState({ currentUser: user.id });
18        });
19      }
20    }
21
22    render() {
23      return _horizon.hasAuthToken() ? (
24        <ChildComponent {...this.props} user={this.state.currentUser} />
25      ) : (
26        <Login />
27      );
28    }
29  }
30
31  return AuthenticatedComponent;
32};
jsx

The user ID will be stored in the state and passed to the component that wraps. This way, when it changes (because it's fetched asynchronously), the child component will be re-rendered.

In the render() function, we check if the authentication token is present so the child component can be rendered. Otherwise, the login page is presented.

In the Login component (components/login.js), we add the logic to present the Github login with the method authEndpoint():

1import React, { Component } from "react";
2import Horizon from "../horizon-container";
3
4const _horizon = Horizon.get();
5
6export default class Login extends Component {
7  handleAuth = () => {
8    _horizon.authEndpoint("github").subscribe(endpoint => {
9      window.location.replace(endpoint);
10    });
11  };
12
13  render() {
14    return (
15      <div>
16        <button className={"login-btn"} onClick={this.handleAuth}>
17          Login with Github
18        </button>
19      </div>
20    );
21  }
22}
jsx

This way, we protect a route (in src/router.js), in this case /message, like this:

1import authenticate from './authenticate-route'
2
3...
4
5export default (
6  <Router history={browserHistory}>
7    <Route path="/" component={MainLayout}>
8      <IndexRoute component={Home} />
9      <Route path="messages" component={authenticate(MessageContainer)} />
10      <Route path="login" component={Login} />
11    </Route>
12  </Router>
13);
jsx

Also, for this application we're going to show a different menu for authenticated users. Open the file src/components/menu.js and modify it so it looks like this:

1import React, { Component } from "react";
2import { Link, IndexLink } from "react-router";
3import Horizon from "../horizon-container";
4
5export default class Menu extends Component {
6  logout = e => {
7    e.preventDefault();
8    Horizon.clearAuthTokens();
9    this.context.router.push("/");
10  };
11
12  render() {
13    var menu = Horizon.get().hasAuthToken() ? (
14      <div className={"menu"}>
15        <IndexLink to="/" className={"menu-option"} activeClassName="active">
16          Home
17        </IndexLink>
18        <Link to="/messages" className={"menu-option"} activeClassName="active">
19          Messages
20        </Link>
21        <a href="#" className={"menu-option"} onClick={this.logout}>
22          Log out
23        </a>
24      </div>
25    ) : (
26      <div className={"menu"}>
27        <IndexLink to="/" className={"menu-option"} activeClassName="active">
28          Home
29        </IndexLink>
30        <Link to="/login" className={"menu-option"} activeClassName="active">
31          Login
32        </Link>
33      </div>
34    );
35
36    return menu;
37  }
38}
39
40Menu.contextTypes = {
41  router: React.PropTypes.object
42};
jsx

Using the method hasAuthToken(), we're going to show a menu with the Messages and Logout options for authenticated users.

The logout function deletes the token and redirects the user to the home page. In order to do this, we need the router object, and since we're using an ES6 class, we have to inject it like this:

1Menu.contextTypes = {
2  router: React.PropTypes.object
3};
jsx

The authentication component passes to the child the ID of the user, so let's modify the message components to use it.

In components/message-container.js you just need to update the handleSubmit() method to get the ID from the properties:

1handleSubmit = () => {
2  if (this.state.text === "" || this.state.author === "") {
3    this.setState({ errorDescription: "The message is required" });
4  } else {
5    this.storeMessage(this.props.user, this.state.text);
6  }
7};
javascript

And pass it to the Messages component:

1render() {
2	return (
3		<div>
4			<div className={'message-container'}>
5				<div>{this.state.errorDescription}</div>
6				<input onChange={this.handleChangeText} className={'message-input'} type='text' placeholder='Enter your message' value={this.state.text}/>
7				<button className={'message-btn'} onClick={this.handleSubmit}>Submit</button>
8				<Messages messages={_messageCollection} user={this.props.user}/>
9			</div>
10		</div>
11	);
12}
jsx

This way, the Messages component can use it to filter the messages:

1export default class Messages extends Component {
2
3	constructor(props) {
4        super(props);
5        this.messageCollection = this.props.messages;
6        this.userId = '';
7        this.state = {messages: []};
8	}
9
10	componentWillReceiveProps(nextProps) {
11        if (nextProps.user !== this.userId) {
12            this.userId = nextProps.user;
13            this.messageCollection.findAll({author: this.userId}).watch().subscribe(
14                (collection) => {
15                    if(collection) {
16                        this.setState({messages: collection});
17                    }
18                },
19                (err) => {
20                    console.log(err);
21                }
22            );
23        }
24	}
25
26	render() {
27        ...
28	}
29}
javascript

Notice that we had to replace componentDidMount() by componentWillReceiveProps(nextProps).

The reason is that componentDidMount() of the child components is invoked before that of parent components. So, when the user ID is received, the child components are re-rendered but the componentDidMount() method of components/messages.js is not executed again with this value, so the changes of that user are not received.

componentWillReceiveProps(nextProps) is invoked when a component is receiving new properties and we can be used to update the state due to a property change before render() is executed.

However, this will make the component continuously re-render (because of the state changes of components/message-container.js), so we have to put a condition to only execute the watch() query when we first receive the user ID:

1componentWillReceiveProps(nextProps) {
2	if (nextProps.user !== this.userId) {
3		this.userId = nextProps.user;
4		this.messageCollection.findAll({author: this.userId}).watch().subscribe(
5			...
6		);
7	}
8}
javascript

Finally, we're ready to test the authentication. Execute npm start to pack the changes and start the server.

The first time you log into the application, it will prompt you to authorize it on Github:

Github authentication

Notice that the menu now changes for (un)authenticated users and, since we're filtering by user ID, the previously entered messages are not shown.

In the logs of the server, you should also see this line:

1warn: Auto-creating index on collection "users": [["id"]]
2warn: Auto-creating index on collection "messages": [["author"]]

In development mode, Horizon will create an index to speed up the queries automatically.

After this, every time you log into the application, if you have a session on Github, it should log you in automatically, otherwise, it will prompt you for your credentials.

Additionally, you can go to https://github.com/settings/developers to view the number of registered user in your application and change the settings if you want.

Integrating Express and Horizon

Now that we have the authentication part done, we just have one final problem to solve.

The routes work by clicking on the links, but what happens when we enter directly the route in the browser:

Router problem

This is because the Horizon server doesn't know about the routes configured in React, it just serves whatever files are in the dist directory.

The solution is to integrate Horizon with a web server that redirects all requests to index.html. For this app, we are going to use Express, but integrating other frameworks like Koa or Happi with Horizon is very similar.

However, we need to make some major changes:

  • We 're not going to start the server with hz serve, which means that a RethinkDB server won't be automatically started.
  • When using Horizon on the client side, the file .hz/config.toml holds the configuration of the framework. When using Horizon on the server side, we need to pass all these configurations to the server when we create it.
  • Because of this, we'll need to configure Express for HTTPS

Let's start by adding the dependencies we're going to need to our package.json file:

1npm install --save express path @horizon/server
bash

Once again, you can force the installation of version 2.0.0 with:

1npm install --save @horizon/[email protected]
bash

Next, let's add a server.js file to the root directory, importing the libraries we're going to need and creating the Express object:

1const express = require("express");
2const https = require("https");
3const path = require("path");
4const fs = require("fs");
5const horizon = require("@horizon/server");
6
7const app = express();
javascript

Now configure the routes for the public files and to redirect all requests to index.html

1// Serve our static stuff like css
2app.use(express.static(path.join(__dirname, "dist")));
3
4// Send all requests to index.html
5app.get("*", function(req, res) {
6  res.sendFile(path.join(__dirname, "dist", "index.html"));
7});
javascript

Next, configure Express to use HTTPS. We can reuse our self-signed certificates:

1const options = {
2  key: fs.readFileSync(path.resolve(__dirname, "./config/tls/horizon-key.pem")),
3  cert: fs.readFileSync(
4    path.resolve(__dirname, "./config/tls/horizon-cert.pem")
5  )
6};
7const PORT = process.env.PORT || 8181;
8
9const server = https.createServer(options, app);
10
11server.listen(PORT, function() {
12  console.log("Express server running at localhost:" + PORT);
13});
javascript

Now that we have an HTTPS server, we configure Horizon to use it. We'll need to copy the token_secret and Github client ID and client secret from the .hz/config.toml file:

1const horizon_server = horizon(server, {
2  project_name: "react_horizon",
3  permissions: true,
4  auth: {
5    token_secret:
6      "NnvpIpep8g9msem6pQHap6g38/wZ0GYQH9/NtXnUTRWlSHT28UtrbAHxxJhi+7673koIJx2Ay5kFX+zHua3fjQ=="
7  }
8});
9
10// Add Github authentication
11horizon_server.add_auth_provider(horizon.auth.github, {
12  path: "github",
13  id: "2660ef72dc60e109b088",
14  secret: "a844d51e652d74a6760ab71815050cba58a70d88"
15});
javascript

As you can see, the server is passed to Horizon along with options that are similar to the ones defined in the .hz/config.toml file. In the above code, we set the project name, enable permissions (more of this in a moment), and use token authentication. You can find more information about all the options here.

Finally, we use the add_auth_provider method to set up the OAuth Github provider. You can extract all this configuration data to an external file if you want (like a config.js file).

By default, Horizon will connect to a RethinkDB server on localhost:28015. We're also enabling permissions (which is actually the default option).

In this configuration, Horizon doesn't allow access to collections by default, even for authenticated users, which means that we won't be able to use neither our message collection nor getting the user ID (from the users table).

To fix this, we need to manually import the permission rules to the database we're going to use. If you execute this command:

1hz schema save -n react_horizon --start-rethinkdb yes -o schema.toml

Horizon will start the development RethinkDB server, and extract the schema, validation rules, collection and index specifications of the react_horizon application (it won't extract the collection's data) as a TOML file, schema.toml. Here's what this file contains:

1# This is a TOML document
2
3[collections.messages]
4[[collections.messages.indexes]]
5fields = [["author"]]
6
7[collections.users]
8[[collections.users.indexes]]
9fields = [["id"]]
10
11[groups.admin]
12[groups.admin.rules.carte_blanche]
13template = "any()"

Since we were using Horizon in development mode (where no permissions are enforced), there are no permissions rules set up, so we will have to add the following to schema.toml:

1[groups.authenticated.rules.read_own_messages]
2template = "collection('messages').findAll({author: userId()})"
3
4[groups.authenticated.rules.write_own_messages]
5template = "collection('messages').store({author: userId(), text: any()})"
6
7[groups.default.rules.read_current_user]
8template = "collection('users').find({id: userId()})"

You can learn about permission rules in this page, but what the above lines do is to allow the authenticated user to read and write their own messages, and to read the users table to get the data by their ID.

Move this file to config/rethinkdb/schema.toml, once again, to keep things organized.

To import these rules to our new database, first you have to start it (in another terminal and, preferably, in another directory) with:

1rethinkdb

And then, execute this command from the root directory of the application:

1hz schema apply -n react_horizon -c localhost:28015 config/rethinkdb/schema.toml

If you go to http://localhost:8080/#tables, you should see the imported databases:

Rethinkdb Imported Databases

And now that we won't be using the files of the .hz directory and the rethinkdb-data directory, you can delete them.

Finally, change the start script on package.json to start the Express server instead of the Horizon development server:

1{
2  ...
3  "scripts": {
4    "start": "webpack && node server.js"
5  },
6  ...
7}
json

When you run the application (don't forget to start the RethinkDB server also), the problem with entering the URL directly in the browser should be solved:

Routes working

You can also test the rest of the functionality to make sure everything is working correctly.

Conclusion

There you have it. We created a single page application with authentication and real-time capabilities using React, React Router, Webpack, Babel, Node.js, Express, and of course, Horizon.

There are some limitations of what you can do with Horizon because, at the time of this writing, it is a relatively new framework. However, it has a lot of features that make real-time programming easier, not without mentioning its growing and strong community.

Remember that the code of the application is on Github and if you have any questions or comments, don' hesitate to contact me.