No Programming, No Life
Dec 30, 2016 • 8 min read

First Step for React.js on Rails

This article describes how to start React on Rails.

Instration Node with Mac Homebrew

brew upgrade
brew install node

Creating React Sample App

npm install -g create-react-app
create-react-app hello-world
cd hello-world
npm start

Using React on Rails

In this section, we are using RubyGem react_on_rails.

At first, add the following to your Gemfile and bundle install:

gem 'react_on_rails'

Commit this to git and please run following commands:

# Run the generator with a sample "Hello World" App with React.js
rails g react_on_rails:install

# Bundle && NPM install:
bundle && npm install

# Start your Rails server:
foreman start -f Procfile.dev

Please see http://localhost:3000/hello_world.

All JavaScript in React On Rails is loaded from npm: react-on-rails. To manually install this, please execute following command like this:

cd client && npm i --saveDev react react-on-rails react-helmet nprogress

Controller

Supporting following flow:

  1. Return HTML in first request
  2. In page transition, return JSON
  3. Do not show JSON when returning with browser back
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  private

  def _action_path
    "#{controller_path}##{action_name}"
  end

  def _common_props
    { actionPath: _action_path }
  end

  def _render_for_react(props: {}, status: 200)
    if request.format.json?
      response.headers['Cache-Control'] = 'no-cache, no-store'
      response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
      response.headers['Pragma'] = 'no-cache'
      render(
        json: {
          rootProps: _common_props.merge(props)
        },
        status: status
      )
    else
      render(
        html: view_context.react_component(
          'Router',
          props: {
            rootProps: _common_props.merge(props)
          }.as_json
        ),
        layout: true,
        status: status
      )
    end
  end
end

Usage example is as follows:

class ArticlesController < ApplicationController
  # GET /
  def index
    _render_for_react(
      props: {
        articles: Article.limit(20)
      }
    )
  end
end

JavaScript

Entry Point

Entry point of shared JavaScript code is like this:

// client/entry_points/main.js
import ReactOnRails from "react-on-rails";
import Router from "../components/Router";

ReactOnRails.register({ Router });

Router

  1. Router selects Component by actionPath
  2. Display progress bar during page transition
  3. Scroll to a top of a page after page transition
  4. Adjust browser history using pushState / popState
import React from "react";
import NProgress from "nprogress";

import Articles from "../components/Articles"
//...

export default class Router extends React.Component {
  static propTypes = {
    rootProps:  React.PropTypes.object,
  };

  static childContextTypes = {
    onLinkClick: React.PropTypes.func,
  };

  componentDidMount() {
    window.addEventListener("popstate", () => {
      this.transitTo(document.location.href, { pushState: false });
    });
  }

  constructor(...args) {
    super(...args);
    this.state = {
      rootProps:    this.props.rootProps,
    };
  };

  getComponent() {
    switch (this.state.rootProps.actionPath) {
      case "articles#index":
        return Articles;
      //...
    }
  };

  getChildContext() {
    return {
      onLinkClick: this.onLinkClick.bind(this),
    };
  };

  onLinkClick(event) {
    if (!event.metaKey) {
      event.preventDefault();
      const anchorElement = event.currentTarget.pathname ? event.currentTarget : event.currentTarget.querySelector("a");
      this.transitTo(anchorElement.href, { pushState: true });
    }
  };

  transitTo(url, { pushState }) {
    NProgress.start();
    $.ajax({
      url: url,
      dataType: 'json',
      cache: false,
      success: function(props) {
        if (pushState) {
          history.pushState({}, "", url);
        }
        this.setState({rootProps: props.rootProps});
        NProgress.done();
        if(typeof window !== 'undefined') {
          window.scrollTo(0, 0);
        }
      }.bind(this),
      error: function(xhr, status, err) {
        NProgress.done();
        console.error(url, status, err.toString());
      }.bind(this)
    });
  };

  render() {
    const Component = this.getComponent();
    return <Component {...this.state.rootProps} key={this.state.requestId} />;
  };
}

When a user clicks a link, a client communicates with the server with XHR and post a state to Router.

So I use the original Link tag instead of a tag.

import React from "react";

export default class Link extends React.Component {
  static contextTypes = {
    onLinkClick: React.PropTypes.func,
  };

  onClick(event) {
    this.context.onLinkClick(event);
  }

  render() {
    return(
      <a onClick={this.onClick.bind(this)} {...this.props}>
        {this.props.children}
      </a>
    );
  }
};

Link example is as follows:

<Link href="/">txt</Link>

Title/Meta Tags

import React from "react";
import Helmet from "react-helmet";

import {siteName, siteBaseUrl} from "../constants/service";

export default class Articles extends React.Component {
  pageUrl() {
    return siteBaseUrl + "/articles";
  }

  pageTitle() {
    return "huga";
  }

  pageDescription() {
    return "hoge";
  }

  render() {
    return(
      <div>
        <Helmet
          title={this.pageTitle()}
          link={[
            {rel: "canonical", href: this.pageUrl()},
            {rel: 'alternate', href: this.pageUrl()},
          ]}
          meta={[
            {property: "og:url",          content: this.pageUrl()},
            {property: "og:title",        content: this.pageTitle()},
            {property: "og:description",  content: this.pageDescription()},
            {name: "description",         content: this.pageDescription()},
          ]}
        />
        //...
  </div>
  );
  }
}

Run npm install in ElasticBeansTalk

This is a script to use webpack in deploy process of ElasticBeansTalk. To use webpack you need to run npm install before rake assets: precompile. (10 is depend on each environment, so please fix it.)

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/pre/10_install_node_modules.sh" :
    mode: "000744"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      set -xe
      EB_APP_STAGING_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_staging_dir)
      cd $EB_APP_STAGING_DIR/client
      npm install
    encoding: plain

Special Thanks

This article has created by the following Japanese articles. Thank you very much, @r7kamura!

Sample App