ReactJs and Rails

Fabien Garcia

Philosophy of the framework

  • Care about the view
  • Care about binding
  • Care about rendering
  • Everything is component

Install with rails

How ?

# Gemfile
gem 'react-rails'
# In the console
bundle install

That was hard right?

Why this gem?

ranking

Benefits of react-rails

  • Provide various react builds to your asset bundle
  • Transform .jsx in the asset pipeline
  • Render components into views and mount them via view helper & react_ujs
  • Render components server-side with prerender: true
  • Generate components with a Rails generator
  • Be extended with custom renderers, transformers and view helpers

Sample App

List records

Controller

# app/controllers/records_controller.rb

class RecordsController < ApplicationController
  def index
    @records = Record.all
  end
end

View

Helper

<%# app/views/records/index.html.erb %>
<%= react_component 'Records', { data: @records } %>

Generate

Coffeescript

# app/assets/javascripts/components/records.js.coffee
  @Records = React.createClass
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'

or with JSX syntax

@Records = React.createClass
  render: ->
    `

Records

`

Components

# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
  ...
  render: ->
    React.DOM.div
      className: 'records'
      React.DOM.h2
        className: 'title'
        'Records'
      React.DOM.table
        className: 'table table-bordered'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record

Single record

@Record = React.createClass
  render: ->
    React.DOM.tr null,
      React.DOM.td null, @props.record.date
      React.DOM.td null, @props.record.title
      React.DOM.td null, amountFormat(@props.record.amount)

New record

# app/assets/javascripts/components/record_form.js.coffee

@RecordForm = React.createClass
  getInitialState: ->
    title: ''
    date: ''
    amount: ''
  render: ->
    React.DOM.form
      className: 'form-inline'
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'text'
          className: 'form-control'
          placeholder: 'Date'
          name: 'date'
          value: @state.date
          onChange: @handleChange
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'text'
          className: 'form-control'
          placeholder: 'Title'
          name: 'title'
          value: @state.title
          onChange: @handleChange
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'number'
          className: 'form-control'
          placeholder: 'Amount'
          name: 'amount'
          value: @state.amount
          onChange: @handleChange
      React.DOM.button
        type: 'submit'
        className: 'btn btn-primary'
        disabled: !@valid()
        'Create record'

handling submitting

# app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
  ...
  handleSubmit: (e) ->
    e.preventDefault()
    $.post '', { record: @state }, (data) =>
      @props.handleNewRecord data
      @setState @getInitialState()
    , 'JSON'

  render: ->
    React.DOM.form
      className: 'form-inline'
      onSubmit: @handleSubmit
    ...

Updating the Parent

# app/assets/javascripts/components/records.js.coffee

@Records = React.createClass
  ...
  addRecord: (record) ->
    records = @state.records.slice()
    records.push record
    @setState records: records
  render: ->
    React.DOM.div
      className: 'records'
      React.DOM.h2
        className: 'title'
        'Records'
      React.createElement RecordForm, handleNewRecord: @addRecord
      React.DOM.hr null
    ...

Real rails app

My react App

Architecture

ranking

Testing with rspec and rails

describe "Integration", js: true, type: :feature do 
it "should see react comment" do
  comment = FactoryGirl.create :comment
  visit root_path
  expect(page).to have_content(comment.text)
  expect(page).to have_content(comment.author)
end

You can check real test here

Codeship Status for garciaf/react_app

Testing component

Still need to figured out how it works but there is official doc for it:

Test Utilities

I18n with rails

We can use the gem i18n-js

And integrate in our component like this, it load the translation from the server side

# config/locales/en.yml
en:
  hello: Hello World
render: function() {
  return (
      
{I18n.t('hello')}
); }

I18n with react

Yahoo provide an ad-don to handle internationalization react-intl other option is i18n-js

Json server side

How to handle Json serialization?

There is many solution out there but I will talk about two of them

Jbuilder

API

Single record

json.set! :author do
  json.set! :name, 'David'
end

# => "author": { "name": "David" }

Array

# @people = People.all

json.array! @people, :id, :name

# => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ]

Nested in the class

class Person
  # ... Class Definition ... #
  def to_builder
    Jbuilder.new do |person|
      person.(self, :name, :age)
    end
  end
end

class Company
  # ... Class Definition ... #
  def to_builder
    Jbuilder.new do |company|
      company.name name
      company.president president.to_builder
    end
  end
end

company = Company.new('Doodle Corp', Person.new('John Stobs', 58))
company.to_builder.target!
# => {"name":"Doodle Corp","president":{"name":"John Stobs","age":58}}

In a view:

# app/views/message/show.json.jbuilder
json.content format_content(@message.content)
json.(@message, :created_at)

json.author do
  json.name @message.creator.name.familiar
  json.email_address @message.creator.email_address
end

Result:

{
  "content": "

This is serious monkey business

", "created_at": "2011-10-29T20:45:28-05:00", "author": { "name": "David H.", "email_address": "david@n.com" } }

Jbuilder

Benefit

  • Really flexible
  • handle nested object
  • Supported by rails
  • Can be implemented in a model
  • Same controller can handle different format easily

Jbuilder

Drawback

  • Hard to keep things DRY
  • Api not so intuitive
  • Hard to use in a view

So what else?

active model serializers

Api

class IosAppSerializer < ActiveModel::Serializer

  include Rails.application.routes.url_helpers

  attributes :id, :name, :app_url
  
  def app_url
    ios_app_path(object)
  end

end

Controller

class IosAppsController < ApplicationController
  def index
    @ios_apps = IosApp.all
    respond_to do |format|
      format.json {render json: @ios_apps }
    end
  end
end

Helper

module ApplicationHelper
  def json_for(target)
    ActiveModel::SerializableResource.new(target).serializable_hash
  end
end

View

= json_for(@ios_apps)

Easy right?

active model serializers

Good point

  • Definition outside the model
  • Can be used everywhere in the application
  • Simple syntax

Building a application

Client side

Backbone react component

We will use just react, backbone and backbone-react-component to glue them together

Overview

App scope

The purpose it to be able to access any component, model or collection in the code

window.MyApp =
  Models: {}
  Collections: {}
  Views: {}

Collection and model

Collection can refer special model via the App scope

# models/post.js.coffee
MyApp.Models.Post = Backbone.Model.extend
# collections/posts.js.cofee
MyApp.Collections.Posts = Backbone.Collection.extend
  model: MyApp.Models.Post

Application file

#= require ./vendor/jquery
#= require ./vendor/underscore
#= require ./vendor/backbone

#= require ./vendor/react
#= require ./vendor/react-dom

#= require ./vendor/backbone-react-component

#= require 'app_scope'

#= require_tree './models'
#= require_tree './collections'
#= require_tree './components'

Component server side

react-rails will mount automatically the component with the script react-ujs and if we use the build in helpert

<%# app/views/records/index.html.erb %>
<%= react_component 'Blog', { data: @posts } %>

Component client side

IosAppBox

@IosAppBox = React.createClass
  mixins: [Backbone.React.Component.mixin]
  componentWillMount: ->
    iosApps = new ResignOps.Collections.IosApps
    iosApps.reset(@props.ios_apps)
    @onCollection @, iosApps
  componentWillUnmount: -> 
    @off(@)
  render: ->
    ` `

Component client side

IosAppList

@IosAppList = React.createClass
  mixins: [Backbone.React.Component.mixin]

  render: ->
    IosAppNodes = @getCollection().map((iosApp) ->
      ``
    )
    `
      {IosAppNodes}
    `

result

But

what happen if we want to display this collection in several place?

Like here

You are screwed!

Let's look for an other solution

Backbone react flux dispatcher

React, backbone, flux, dispatcher and there friends join the party

Overview

The dispatcher

@IosAppDispatcher = new Flux.Dispatcher()

Open question: Do we need more than one dispatcher?

Store

IosApp = Backbone.Model.extend
  urlRoot: '/ios_apps/'

IosAppCollection = Backbone.Collection.extend
  model: IosApp
  url: '/ios_apps'
  initialize: ->
    @dispatchToken = window.IosAppDispatcher.register (payload) =>
      switch payload.actionType
        when "ios_app-delete"
          @get(payload.ios_app).destroy()
        when "ios_app-add"
          @add(payload.ios_app, {merge: true, sort: true})

@IosAppStore = new IosAppCollection()

A store is just an instance of a collection or model

Component

IosAppStore = @IosAppStore
@IosAppThumbs = React.createClass
  getInitialState: ->
    {
      IosAppStore: IosAppStore
    }
  componentDidMount: ->
    @state.IosAppStore.on "all", =>
      @forceUpdate()
    , @
  render: ->
    IosAppNodes = @state.IosAppStore.map((iosApp) ->
      ``
    )
    `{IosAppNodes}`

Triger action from the component

IosAppDispatcher = @IosAppDispatcher

@IosAppRow = React.createClass
  destroy: (e) ->
    e.preventDefault()
    IosAppDispatcher.dispatch
      actionType: 'ios_app-delete'
      ios_app: @props.iosApp

result

Creating

result

Destroying

Booth list are sync because they share the same store

Happy?

Not completely

Still few open questions

  • Where or who should populate the store?
  • When using a prop and when a state?
  • John snow is really dead? I mean forever dead?

Sources

Question ?

ranking