On my radar: RethinkDB + React.js + Rails

… and check why 5600+ Rails engineers read also this

On my radar: RethinkDB + React.js + Rails

Sometimes you hear positive things about a technology and you put it on your radar. To explore it when you have some time. To get a feeling if it is fun at all. To mix and match things together to see what comes out of it.

For me recently it was RethinkDB . Its slogan says The open-source database for the realtime web. Interesting enough to get me curious. RethinkDB pushes JSON to your apps in realtime also sounded good.

I was sick one week ago so I had a moment to give it a try.

If RethinkDB pushes changes and React can automatically re-render a page effectively it would seem that merging these two technologies together could give us nice real time page updates. We just need a glue technology that would connect the DB to the browser and authorize the request. Let’s go with Rails.

If you want to learn the basics of RethinkDB I highly recommend their Ten-minute guide with RethinkDB and Ruby . The installation process with provided OS X and Ubuntu packages was very simple for me as well.

Building blocks

We start by adding puma, nobrainer and react-rails to our Gemfile.

We are going to use Puma because going with multi-threaded servers is fastest way for us to start enjoying the results.

NoBrainer is a Ruby ORM for RethinkDB. Similar in taste to ActiveRecord.

And finally react-rails is fastest way to start using react in rails environment.

Starting point

If we go with rails g scaffold Article title:string text:string we will have a basic structure generated. But it will use NoBrainer instead of ActiveRecord. Our document looks like that:

class Article
  include NoBrainer::Document
  include NoBrainer::Document::Timestamps

  field :title, :type => String
  field :author, :type => String
end

You can find out more about integrating RethinkDB with Rails in their own documentation. We are gonna leave the entire write side untouched. But we will fiddle with the read part. How we display the document.

React component

Instead of using html & erb to display an article, we will play with a React component. When written with CoffeeScript it looks similar to HAML. Of course you could go with JSX if that’s your flavor.

DOM = React.DOM

window.ShowArticle = React.createClass
  name: "ShowArticle"
  render: ->
    DOM.div null,
      DOM.p null,
        DOM.strong null, "Title: "
        @props.title
      DOM.p null,
        DOM.strong null, "Text: "
        @props.text

@props are the properties passed to the component when rendered.

First render

We are going to use react_component helper that comes from the react-rails gem. It makes easier to start react components that will run when a browser fetches the page.

And with prerender: true they even render the component server-side first and then react.js in a browser handles the lifecycle of that component. And interactions with it. And all that stuff that your UX is responsible for.

<p id="notice"><%= notice %%></p>

<div class="well bs-component">
  <%= react_component('ShowArticle',
         @article.to_json,
         prerender: true,
         id: 'article',
         data: {reactive: start_show_path})
  %%>
</div>

<%= link_to 'Edit', edit_article_path(@article) %%> |
<%= link_to 'Back', articles_path %%>

data: {reactive: start_show_path} is a path for URL that will be used for streaming changes. Let’s dive into it.

SSE in Browser

So we’ve got the first render covered. But we need to make this component auto updates when the data changes. We are going to use Server Sent Events for that. It’s a browser API for one way, server to browser communication over HTTP connection. It even has automatic re-connections built-in.

ShowArticleFactory = React.createFactory(ShowArticle)

$ ->
  $('[data-reactive]').each (_nop, element) ->
    reactivePath = $(element).attr('data-reactive')

    source = new EventSource(reactivePath);
    source.addEventListener 'message', (e) ->

      React.render(
        ShowArticleFactory( JSON.parse(e.data) )
        element
      )

We look for data-reactive elements and make a connection to the URL. On event we ask react to re-render a new version of the component in the same place.

SSE in Rails

This is just a tiny wrapper for formatting data according to SSE spec.

require 'json'

class JsonSSE
  def initialize(io)
    @io = io

  end

  def write(object)
    @io.write "data: #{JSON.dump(object)}\n\n"
  end

  def close
    @io.close
  end
end

For SSE streaming in Rails I used ActionController::Live. You can read a great blog-post by Aaron Patterson where he introduced Live Streaming in 2012 to get familiar with it. Yep, it was that long time ago. And Rails documentation for ActionController::Live

class StartController < ApplicationController
  include ActionController::Live

  def show
    response.headers['Content-Type'] = 'text/event-stream'

    sse = JsonSSE.new(response.stream)

    article = RethinkDB::RQL.new.table( Article.table_name ).get(Article.last.id)
    article.changes.run(NoBrainer.connection.raw).each do |change|
      sse.write(change['new_val'])
    end
  rescue *client_disconnected
  ensure
    sse.close rescue nil
    NoBrainer.disconnect rescue nil
  end

  private

  def client_disconnected
    return ActionController::Live::ClientDisconnected, IOError
  end
end

Here we use the feature of changefeeds from RethinkDB. You can subscribe to changes from a table, a single document or even a query and be notified every time something changed. In our example we subscribe to changes from one document, the last Article:

RethinkDB::RQL.new.table( Article.table_name ).get(Article.last.id)

This syntax mixes higher-level API (Nobrainer ORM) with low-level API (RethinkDB official driver) but that’s how I managed to get it work.

You can do much more with changefeeds but that’s what I needed for our basic use-case.

Final effect

Surprisingly (or not) it works. You can watch the 20s demo.

You can see the whole code on github arkency/rethinkdb-reactjs .

Dragons

I had to use config.cache_classes = true and config.eager_load = true to get SSE working in development mode.

I set the config.per_thread_connection = true config option for NoBrainer as we use puma, a multi-threaded web server.

I have a feeling that react_component() with prerender: true is not very performant but I haven’t benchmarked yet. It might highly depend on the JS engine and the ruby version that you build your app with. But that’s my gut feeling for now. I want to truly benchmark one day.

The RethinkDB query from our Live Controller is blocking and taking one thread out of the puma’s pool. This can lead to thread pool exhaustion if too many people are connected and the pool size is too small. But one of the next things I want to investigate is sending the stream of changes with EventMachine and/or Thin instead of Puma. This should be possible as the official driver comes with the em_run method which can be used in EventMachine single-threaded non-blocking environment and should scale much better.

Also the thread used by SSE is not stopped if the user navigates away and the browser disconnects. That is because as I said we are waiting (and blocking) for changes from RethinkDB. If those changes occur the attempt to write to the disconnected browser will fail, and a proper exception will be raised (that we catch) so we can end this thread.

People using redis pub-sub experienced similar problem and as a workaround they publish ping every now and then. You could achieve similar thing with RethinkDB:

Subscribe to both notifications.

rql = RethinkDB::RQL.new
rql.table( Article.table_name ).filter({id: Article.first.id}).union(
  rql.table( "pings" ).filter({id: Process.pid})
).changes.run(NoBrainer.connection.raw).each do |change|
  # ...
end

Send pings.

r.db("rethinkapp_development").table("pings").
  insert({id: Process.pid, on: Time.now.to_i, ping: "ping"}).run

loop do
  r.db("rethinkapp_development").table("pings").
    filter{|ping| ping["id"].eq(Process.pid)}.
    update(on: Time.now.to_i).run

  sleep(10)
end

Final thoughts

Despite many dragons I have a feeling that there is a big potential in RethinkDB. I will keep it on my radar and explore more deeply.

You might also like