Introducing React Horizon

In this article I introduce a work in progress idiomatic solution for React applications built on top of Horizon’s client library which can be used to build realtime applications with RethinkDB Horizon platform.

What is Horizon?

Horizon is a new technology for realtime data built on top of RethinkDB. It’s an open source alternative to Firebase which allows to build simple “serverless” applications and integrate into your own backend when needed. Horizon is different from Meteor in a way that it’s responsible only for data flow between client and server, everything else should be managed by developers. It seems like a good replacement for Firebase when building a system that later will scale into fully controlled backend.

React Horizon

$ npm i react-hz

React Horizon is inspired by react-redux and react-relay libraries. It is available as react-hz package on NPM. The whole point is to provide declarative binding to Horizon’s client library and make it easier to use and integrate with React applications. Client library provides facilities for data mutation, subscriptions and connection status handling. All of these features are expressed declaratively as a set of React components.

High-level architecture overview

Establishing connection

Connection to Horizon server is established by calling Horizon constructor function. Returned value is a function that is used to access collections of data and manipulate it using Collection API.

const hz = Horizon({ host: 'localhost:8181' });
hz('messages').find({ sender: 'John' }).limit(10);

Integrating with React

In order to allow components data access via Collection API a special component HorizonProvider should be defined as a top-level component in your app. It accepts an instance of Horizon constructor as instance prop and passes it down using React’s context.

import { HorizonProvider } from 'react-hz';
const horizonInstance = Horizon();
<HorizonProvider instance={horizonInstance}>
<App />
</HorizonProvider>

Client demand is specified by colocating Horizon queries on React components using connect function that expects React component and config object as arguments and returns a container component which has an access to an instance of Horizon. Container component includes shouldComponentUpdate optimisation and passes its props into wrapped component. Data demand consists of subscriptions and mutations.

import { connect } from 'react-hz';
const AppContainer = connect(App, {
subscriptions: {
messages: (hz) => hz('messages')
},
mutations: {
addMessage: (hz) => (message) => hz('messages').store(message)
}
});

Now subscriptions and mutations are available as props in App component with same names as in config object.

class App extends Component {
  render() {
    const { messages, addMessage } = this.props;
const handleClick = () => addMessage('new message');
    return (
<div>
<ul>
{messages.map(({ text }) => {
return <li key={text}>{text}</li>;
})}
</ul>
<button onClick={handleClick}>Add message</button>
</div>
);
}
}

Subscriptions

Subscription function should accept hz function as the first argument which should be used to construct a query using Collection API and return it. Behind the scenes a higher-order React component, which is applied to specified component, will actually subscribe to declared subscription queries and pass data into wrapped component as props with names of corresponding subscriptions. All subscriptions are unsubscribed automatically on componentWillUnmount.

Mutations

Mutation function should also accept hz function. But since data mutation most of the time requires some input, mutation function should return a function which accepts input as arguments, constructs Horizon query and executes it with specified input. Mutations will be passed into wrapped component as props with corresponding names. There’s a number of mutation operations provided by Collection API:

  • store — insert one or more new documents into a collection.
  • remove — delete a single document from a collection.
  • removeAll — delete multiple documents from a collection.
  • replace — replace one or more existing documents within a collection.
  • upsert — insert one or more documents into a collection, replacing existing ones or inserting new ones based on id value.

Additionally it is allowed to create generic mutations. Generic mutation is a function which returns Horizon query. It will be passed into component directly as a prop, thus it’s possible to call any type of mutation operations from within component. Here’s a usage example of both types of mutations.

class App extends Component {
  render() {
    const { messagesMutation, addMessage } = this.props;
    const handleAdd = () => addMessage('new message');
const handleRemove = () => messagesMutation.remove(12);
    return (
<div>
<button onClick={handleAdd}>Add message</button>
<button onClick={handleRemove}>Remove message</button>
</div>
);
}
}
const AppContainer = connect(App, {
mutations: {
messagesMutation: (hz) => hz('messages'),
addMessage: (hz) => (message) => hz('messages').store(message)
}
});

Advanced usage

Applications with multiple routes usually have separate layout component for every route. Taking inspiration from Relay library React Horizon provides a special component HorizonRoute. The component makes use of Horizon’s API methods for receiving connection status updates which allows developers to render different components in response to different connection states.

<HorizonRoute
renderConnecting={() => <h1>Connecting...</h1>}
renderDisconnected={() => <h1>You are offline</h1>}
renderConnected={() => <h1>You are online</h1>}
renderSuccess={() => <h1>Hello!</h1>}
renderFailure={(error) => <h1>Something went wrong...</h1>} />

It is intentionally has route word in its name, but it can be used not only as a top-level component for layouts. For example HorizonRoute can render parts of UI which should be disabled when connection is lost. Notice that every connection status rendering function by default returns null, which means that if you are not going to use some of those handlers, you still need to implement them, otherwise when connection status changes you’ll see blank screen. Most likely this behaviour will be changed later.

Limitations

For now Horizon’s client library can not be used outside of the browser environment because it has hardcoded parts of code which will work only in browser. When this limitation will be removed, React Horizon will be available for React Native out of the box.

Also it seems like using Horizon’s client library as JavaScript module is not possible because it breaks build process. So for now it is preferred to use global Horizon variable provided by automatically served JavaScript file when running on Horizon server.

<script src="/horizon/horizon.js"></script>
<script src="/bundle.js"></script>

GraphQL support?

It is not in the scope of this library, but Horizon team has promised to deliver GraphQL adapter soon. Follow this issue on GitHub to track progress.

Example application

I’ve built an example movie tickets online booking application. Check the source code to learn how to setup build process and structure your application for running with Horizon server. Make sure to complete all prerequisites before running the app.

React Horizon is still a work in progress and it might change in future. It’s totally open for contributions, testing and pull requests are very welcome.

Resources