One of the most recent debates in the web development world has been over the choice of constructing the view layer of modern applications. In this guide, we will compare the React and the Angular 2 integrations using Ruby on Rails. The two approaches will be analyzed in terms of ease of integration and use with Rails.
Because the focus on the tutorial is on getting React or Angular integrated into a Rails application, the Rails application itself will be as simple as possible - it will contain one action that will return an arbitrary JavaScript Object Notation (JSON) that is going to be rendered on the page using React or Angular. Open up your terminal or command prompt and type:
1rails new sampleapp
This will scaffold a default Rails application and install all the required dependencies to make it run. Go to the directory of the application and locate the Gemfile. For this application, you will need only the following gems:
1#Gemfile.rb
2gem 'rails'
3gem 'sqlite3'
4gem 'sprockets'
5gem 'rack-cors'
In your console, run:
1bundle install
This will install the gems in the Gemfile. To configure rack-cors
, add this code snippet to the configuration file of the Rails application:
1 #config/application.rb
2module Starterapp
3 class Application < Rails::Application
4 #...
5 config.middleware.insert_before 0, 'Rack::Cors' do
6 allow do
7 origins '*'
8 resource '*', :headers => :any, :methods => [:get, :post, :options]
9 end
10 end
This configuration will permit all types of requests from any source (\*
denotes any address) to be accepted. For development, this setup is acceptable. However, for production, you need to replace the asterisk with the URL of your client-side application.
The next step is to make a controller and an action that will return JSON data and make a route for these data. For the sake of simplicity and for the tutorial, let's put the action in the application controller:
1 #app/controllers/application_controller.rb
2class ApplicationController < ActionController::Base
3 #...
4 def index
5 respond_to do |format|
6 format.json { render json: {some: 'data'} }
7 end
8 end
9 #...
10end
An action called index
is going to respond with the {some: 'data'}
JSON when requested through http://localhost:3000. Let's put a route for the action:
1#config/routes.rb
2Rails.application.routes.draw do
3 get '/api' => 'application#index', defaults: { format: :json }
4end
Angular 2 has two specifics - it is a framework, meaning that it only provides general functionality and can be modified to fit users' needs, and it uses TypeScript. These specifics come with certain requirements when it comes to integration with the Rails application:
Because Angular 2 is a framework and not a library, it would be best if is put in a separate directory where all its files are going to reside. This means that, instead of putting it into the Rails asset pipeline (app/assets
), the Angular 2 app will reside in the Rails application's public
directory, separated from the compilation and the logic of the Rails application. This will allow us to distinguish the concerns of using the Rails and the Angular 2 applications and their dependencies.
Angular 2 uses TypeScript, a superset of JavaScript. As of today, there isn't a way for TypeScript to be implemented into Rails' asset pipeline, which means that a transpiler has to be configured in the root directory of the Rails application. Transpilers (short for transcompilers) in JavaScript are tools that read code (or CoffeScript or similar) and transpile it to pure JavaScript that can be interpreted by the browser.
Because of TypeScript's requirements, there are three files that need to be created in the root directory in order for the environment to be set up for an Angular 2 application.
To install these requirements, you need to have the Node Package Manager (npm) installed on your computer. Let's go through each of the files:
The package.json file that contains a list of all the packages required to integrate Angular 2 with Rails:
1{
2 "name": "rails-ng2",
3 "version": "0.0.1",
4 "scripts": {
5 "tsc": "tsc",
6 "tsc:w": "tsc -w",
7 "typings": "typings",
8 "postinstall": "typings install"
9 },
10 "license": "ISC",
11 "dependencies": {
12 "angular2": "2.0.0-beta.15",
13 "systemjs": "0.19.26",
14 "es6-shim": "^0.35.0",
15 "reflect-metadata": "0.1.2",
16 "rxjs": "5.0.0-beta.2",
17 "zone.js": "0.6.10",
18 "intl": "^1.0.1"
19 },
20 "engines": {
21 "node": "5.3.0"
22 },
23 "devDependencies": {
24 "typescript": "^1.8.10",
25 "typings": "^0.7.12"
26 }
27}
Paste the contents in a file in the root directory of the Rails application and name it package.json. The file contains typings, a package that is used for configuring the behaviour of TypeScript and the typescript package itself as devDependencies (dependencies of the dependencies). Obviously, angular2 is also one of the packages as well as libraries such as systemjs and es6-shim that add EcmaScript 6 functionality, which is requried for Node 5.
Also in your root directory, create a file named typings.json. This file will be used to configure the dependencies of TypeScript after the packages are installed. You can also configure TypeScript dependencies manually by running typings install
in your console.
1{
2 "ambientDependencies": {
3 "es6-shim":
4 "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd",
5 "jasmine":
6 "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#5c182b9af717f73146399c2485f70f1e2ac0ff2b"
7 }
8}
The last file you need to create in the root directory is tsconfig.json:
1{
2 "compilerOptions": {
3 "target": "es5",
4 "module": "system",
5 "moduleResolution": "node",
6 "sourceMap": true,
7 "emitDecoratorMetadata": true,
8 "experimentalDecorators": true,
9 "removeComments": false,
10 "noImplicitAny": false,
11 "rootDir": "public"
12 },
13 "exclude": ["node_modules", "typings/main", "typings/main.d.ts"]
14}
The file contains standard configuration for the behavior of TypeScript in the Angular 2 application. One thing that requires paricular attention is the rootDir
property which defines that the Angular 2 application will reside in the public
directory.
After these three files are added to the root directory of the Rails application, write the following command in your console:
1 npm install
Wait as the packages are installed. Once the command completes, there will be an extra directory named node_modules
in the root directory that will contain the installations of all the packages.
In order to run the configuration of the typescript, run:
1 npm run tsc
The configuration is almost finished, but there is a gotcha - the node_modules
directory will not load in the application's assets since it is not in the app/assets
directory. Thus, the directory must be added explicitly in the configuration of the Rails application:
1#config/application.rb
2module Starterapp
3 class Application < Rails::Application
4 #.. CORS configuration
5
6 # Explicitly add the 'node_modules' directory
7 config.assets.paths << Rails.root.join('node_modules')
8 end
9end
The Rails application is now ready to load an Angular 2 application that resides in its public
directory. To start off, let's create a root html document that is going to load all the JavaScript files:
1<!-- public/index.html -->
2<html>
3<head>
4 <!-- 1. Load libraries -->
5 <script src="/assets/intl/dist/Intl.js"></script>
6 <script src="/assets/intl/locale-data/jsonp/en.js"></script>
7
8 <script src="/assets/es6-shim/es6-shim.min.js"></script>
9 <script src="/assets/systemjs/dist/system-polyfills.js"></script>
10
11 <script src="/assets/angular2/bundles/angular2-polyfills.js"></script>
12 <script src="/assets/systemjs/dist/system.src.js"></script>
13 <script src="/assets/rxjs/bundles/Rx.js"></script>
14 <script src="/assets/angular2/bundles/angular2.dev.js"></script>
15 <script src="/assets/angular2/bundles/http.dev.js"></script>
16 <script src="/assets/angular2/bundles/router.dev.js"></script>
17</head>
18<body>
19<script>
20 System.config({
21 map: {
22 'app': '/app',
23 },
24 packages: {
25 'app': {
26 format: 'register',
27 defaultExtension: 'js'
28 }
29 }
30 });
31 // and to finish, let's boot the app!
32 System.import('app/boot');
33</script>
34
35<base href="/">
36<app-router></app-router>
37</body>
38</html>
Between the tags, the systemJS library will configure the modules and import the app/boot
file, which is going to be included later in the guide. Another interesting snippet in the file is theapp-router
tag, where the built-in Angular 2 router component is going to be mounted.
In the public
directory, add an app
directory. This is where all the Angular 2 files will be placed. Let's start with the first component - home.component
1// public/app/home.component.ts
2import {Component, OnInit} from 'angular2/core'
3import {RouteParams} from 'angular2/router'
4import {Http, HTTP_PROVIDERS} from 'angular2/http';
5@Component({
6 selector: 'home',
7 templateUrl: '/app/home.component.html'
8})
9export class HomeComponent {
10 message: string;
11 constructor( public http: Http){
12 this.http.get('http://localhost:3000/api')
13 .subscribe(
14 data => this.message = data.json().some,
15 err => console.log(err)
16 );
17 }
18}
One of the most notable features of TypeScript is that it provides classes and ability to import
and export
snippets of code. Such features make the code more maintainable and more familiar to programmers who are not used to JavaScript's idiosyncracies.
Here, you can see how the Http
provider is imported into the file and then made available in the constructor of the HomeComponent
class. Doing http requests is done similarly to Angular 1's $http.get()
service, but the syntax of the promise is handled a little bit differently - subscribe
acts similarly to $promise.then()
, and the functions for resolving the promise use EcmaScript 6's arrow syntax.
To make it more familiar, the Angular 2 data => this.message = data.json().some
translates to function(data) { $scope.message = data.some }
in Angular 1. Another interesting feature is that variables can have a strong type, as it is demonstrated with the message: string;
component variable.
Now let's add the template of the component:
1<!-- public/app/home.component.html-->
2<h2>Rails Angular 2</h2>
3{{message}}
The template will simply print the message that was fetched from the Rails application.
The first component is ready, but there must be a way through which it can be reached withing the Angular 2 application. Because Angular 2 is a framework, it comes with its own built-in router that can be imported and configured to your liking:
1//public/app/app_router.component.ts
2import { Component } from "angular2/core";
3import { RouteConfig, ROUTER_DIRECTIVES } from "angular2/router";
4
5@Component({
6 selector: "app-router",
7 template: "<router-outlet></router-outlet>",
8 directives: [ROUTER_DIRECTIVES],
9 styles: []
10})
11@RouteConfig([])
12export class AppRouterComponent {}
In this file, the router component is imported and configured. You can see that the component is bound to the app-router
tag. The built-in router directives are put under the directives
property. @RouteConfig
contains an array of route objects.
A route object can contain a path , name and a component that it uses. Let's add home.component
in there:
Import the component
1import { HomeComponent } from "./home.component";
Put it in @RouteConfig
1 @RouteConfig([
2 { path: '/', name: 'Home', component: HomeComponent }
3])
The last thing that needs to be done is to add the boot
file that is going to bootstrap, or initiate, the application:
1//public/app/boot.ts
2import { provide } from "angular2/core";
3import { bootstrap } from "angular2/platform/browser";
4import { AppRouterComponent } from "./app_router.component";
5import { HTTP_PROVIDERS } from "angular2/http";
6import { ROUTER_PROVIDERS } from "angular2/router";
7
8bootstrap(AppRouterComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS]);
You can regard boot.ts
as the initialization file of the Angular 2 application. It imports the AppRouterComponent
that was defined earlier and bootstraps it, making all its routes and components reachable through their paths.
This step completes the Angular 2 integration into your Rails application. Start the server using:
1rails s
Open your browser and go to http://localhost:3000. You should see the
Unlike Angular 2, which is a full-fledged framework, React is simply a library that provides bare-bones features for constructing user interfaces. React uses JSX, or Java Serialization to XML, to render its components. JSX is much simpler and lighter than TypeScript, and, unlike TypeScript, JSX can be integrated into Rails' asset pipeline. As a result, React integration becomes sufficiently faster.
Because React's JSX easily integrates with the asset pipeline, you can set up the environment by simply installing the react-rails gem. It comes with a set of useful features - server-side rendering, component generators and more. Add the gem to your Gemfile:
1# Gemfile.rb
2gem 'react-rails'
Install the gem:
1bundle install
Run the generator provided by the gem to make the Rails application work with React:
1rails g react:install
This will create an initialization file named component.js
in the app/assets
directory and a new directory, app/assets/javascripts/components/
for the rest of the components. It is also going to add the React dependencies to the asset pipeline:
1//app/assets/javascripts/application.js
2
3//= require react
4//= require react_ujs
5//= require components
React is configured, now all we need is to put a controller, a view, and a route for our Ruby on Rails application to render the components.
First, let's create a simple controller. Go to app/controllers
and create a file named site_controller.rb
:
1#app/controllers/site_controller.rb
2
3class SiteController < ApplicationController
4 def index
5 end
6end
Put the controller's index
action as a root for your Rails application:
1#app/config/routes.rb
2
3Rails.application.routes.draw do
4 get '/api' => 'application#index', defaults: { format: :json }
5 root to: 'site#index' #add the route for the React component
6end
This is everything that is required to setup the environment React for the Rails application. Next, we are going to render the data from the server.
To make a new component, you can simply run the generator and it will be created for you:
1 rails generate react:component Item --es6
The generator will create an component in app/assets/javascripts/components
in EcmaScript 6 syntax. Open the file and put the following code snippet:
1//app/assets/javascripts/components/item.es6.jsx
2class Item extends React.Component {
3 constructor(props, context) {
4 super(props, context);
5 this.state = { item: "" };
6 }
7
8 render() {
9 return (
10 <div>
11 <h2> Rails React starter </h2>
12 <div> {this.state.item}</div>
13 </div>
14 );
15 }
16}
constructor()
is used to initialize the component's state variables. In the constructor, an empty Item
object will be initialized in the component's state. super
is a common Object-Oriented pattern, here it's used to inherit the props and context from the React.Component
class.
The render()
function is used to render html into the component. In this case, it will render the item
state of the component. this.state
contains the state of the component, which is usually contained in private or component-specific variables. item
is one of these variables. When there are several nested components however, the data between them is passed through this.props
.
What we want is to render the data from the server into item
:
1//app/assets/javascripts/components/item.es6.jsx
2class Item extends React.Component {
3 //constructor
4 componentDidMount() {
5 $.getJSON("/api", response => {
6 this.setState({ item: response.some });
7 });
8 }
9 //render
10}
componentDidMount()
is a method that will be called when the component becomes mounted into the DOM. Simply said, it is called when the component is rendered on the page. In it, the $.getJSON
function makes a request to localhost:3000/api
and uses EcmaScript 6's arrow function to get the callback when the request succeeds. this.setState() will set the item
property with the property of the response
object, which will contain { some: 'data' }
.
Last, make a view to render the component. Go to app/views
, create a directory named site
and create a file named index.html.erb
. Put the following snippet in it:
1<!-- app/views/site/index.html.erb -->
2<%= react_component 'Item' %>
react_component
is a built-in helper method provided by react-rails that is going to render the Item
component into the Rails view.
With this step, the Rails application integrates React. Go to http://localhost:3000 and see the results. If you encounter any problems, check the GitHub repository .
Here is a overview of the three different ways the view layer in Rails can be hanlded:
React | Angular 2 | Rails |
---|---|---|
Root/Parent component | Service | View layer |
React | Angular 2 | Rails |
---|---|---|
app/assets/javascripts directory | public or a separate directory | app/views directory |
React | Angular 2 | Rails |
---|---|---|
JSX | TypeScript | ERB |
Both React and Angular 2 come with their pros and cons. However, in terms of integration and configuration with Rails, React is a clear winner. The integration with the asset pipeline and server-side rendering make React far simpler to set up and more configurable with Ruby on Rails. Angular 2, on the other hand, tends to be bulkier and trickier to implement. Part of the issue is TypeScript's complexity when it comes to integrating with the asset pipeline. Furthermore, Angular 2 is quite new to the Rails ecosystem, so implementations have not yet been adequately generated.