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} />;
};
}

Link

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 beforerake 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

๐Ÿ–ฅ Recommended VPS Service

VULTR provides high performance cloud compute environment for you. Vultr has 15 data-centers strategically placed around the globe, you can use a VPS with 512 MB memory for just $ 2.5 / month ($ 0.004 / hour). In addition, Vultr is up to 4 times faster than the competition, so please check it => Check Benchmark Results!!