First Step for React.js on Rails

This article describes how to start React on Rails.


Instration Node with Mac Homebrew

1
2
brew upgrade
brew install node

Creating React Sample App

1
2
3
4
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:

1
gem 'react_on_rails'

Commit this to git and please run following commands:

1
2
3
4
5
6
7
8
# 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:

1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
// 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

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

Title/Meta Tags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.)

1
2
3
4
5
6
7
8
9
10
11
12
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