Rails and React and Alt « Tour of Heroes » tutorial

Rails and React with Alt – ES6 – without npm

React could stand alone if you have a very simple application as shown above. However, with a big one, you will soon lose track of which components exchanges with which one, and what impacts may have such change in a component. You need an architecture that will structure a little bit your application. That is where FLUX arrives! You can get some information here: https://facebook.github.io/flux/.

Flux has multiple implementations but James K Nelson gives a nice comparison here:  Which Flux Implementation Should I Use With React? We are going to test Alt implementation, as it look easier than the others.

From now on, we are also going to use ES6. I am not a big fan of ES6, as I find it almost as verbose as plain Javascript (ie ES3 or ES5) but it brings a few interesting new features (classes, arrow functions, promises, …), and it is the langage that is more and more used in documentations you can find on the Web. If you want to keep using plain Javascript, just reuse the code above and follow the Plain Javascript chapter of Alt documentation.

Let’s start with a new rails project. I will go over all the steps again so that you do not have to read from the beginning…

1 – Rails configuration

1.1 – Create a new rails project

rails new reactjs_in_ES6_with_alt_project

1.2 – Update your Gemfile

In your Gemfile, uncomment therubyracer gem, and add both the react-rails and the sprockets-es6 gems to your Gemfile. The first gem enables you to use React in your project and the second one to transform ES6 to Plain Javascript properly. If you do not install the sprockets-es6 gem, you may still interpret some ES6, but you will encounter javascript error message such as: Class constructor […] cannot be invoked without ‘new’

In Gemfile:

source 'https://rubygems.org'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.5.2'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug'
end

group :development do
  # Access an IRB console on exception pages or by using <%= console %> in views
  gem 'web-console', '~> 2.0'

  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
end

#Use reactJS (after bundle install, type: "rails g react:install")
gem 'react-rails'

#Enables ES6 (otherwise you may encounter javascript errors with the current es6 transpiler)
gem 'sprockets-es6'

And type the following in your console:

rails g react:install

As the react-rails gem documentation explains:

This will:

  • create a components.js manifest file and a app/assets/javascripts/components/ directory, where you will put your components
  • place the following in your application.js:

1.3 – Get ReactRouter Javascript library and put it in vendor directory

We want the routing to be managed by the client, not the server, so we need the ReactRouter Javascript library. You can find the link in the github project: react-router. And download it from here: https://npmcdn.com/react-router/umd/ReactRouter.min.js. You can reference it in your application.html.erb file, but I suggest to download it and put it in your vendor/assets/javascripts directory.

get https://npmcdn.com/react-router/umd/ReactRouter.min.js file and put it there: vendor/assets/javascripts/ReactRouter.min.js

1.4 – Get Alt Javascript library and put it in vendor directory

The Alt guide says that you can get a browser build of alt here. So download this file and copy it in your vendor/assets/javascripts directory.

get https://cdn.rawgit.com/goatslacker/alt/master/dist/alt.js file and put it there: vendor/assets/javascripts/alt.js

1.5 – Update your routes

It is exactly the same thing as before.

The first line says to start the application with application controller and index action. The second one will be used to get the list of heroes from the server, show a hero, and update it. And the last line enables to redirect all other url to application controller index action.

In config/routes.rb:

Rails.application.routes.draw do
  root 'application#index'

  resources :heroes, only: [:index, :show, :update]

  match "*path", to: 'application#index', via: [ :get, :post ]
end

1.6 – Update your application controller

Here, we call the react_component method (from react-rails gem) to call the React StaticRoute component. It will be the starting point of React.

In app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  
  def index
    render inline: "<%= react_component('StaticRoute') %>", layout: 'application'
  end
end

1.7 – Create a hero controller

Here we use the same controller as with Angular2. I remind you that it is only for testing purposes, for a real application you have to use database and models (and do not forget to keep your controllers as light as possible!).

In app/controllers/heroes_controller.rb:

class HeroesController < ApplicationController

  def index 
    heroes_list_json = []
    #we get all the heroes from the file
    File.open(Rails.root.to_s + '/tmp/heroes_list', 'r').each_line { |l| heroes_list_json << Hash[*l.chomp.split(/,|:/)] }
    respond_to do |format|
      format.json { render json: heroes_list_json }
    end
  end
  
  def show
    hero = {}
    #we get only the hero which match the params[:id]
    File.open(Rails.root.to_s + '/tmp/heroes_list', 'r').each_line { |l| hero = Hash[*l.chomp.split(/,|:/)] if l.split(',')[0].split('id:')[1] == params[:id] }
    respond_to do |format|
      format.json { render json: hero }
    end
  end
  
  def update
    original_file_path = Rails.root.to_s + '/tmp/heroes_list'
    temp_file_path = Rails.root.to_s + '/tmp/heroes_list.tmp'
    
    #we create a temporary file in which we copy the line of the original file. we only change the line where the id is equal to params[:id]
    temp_file = File.open(temp_file_path, 'w')
    File.open(original_file_path, 'r').each_line do |line|
      if line.split(',')[0].split('id:')[1] == params[:id]
        temp_file.puts "id:#{params[:id]},name:#{params[:name]}"
      else
         temp_file.puts line
      end
    end
    temp_file.close
    FileUtils.mv(temp_file_path, original_file_path)
      
    respond_to do |format|
      format.json { render nothing: true }
    end
  end

end

1.8 – Create a heroes_list file (only as example! use database in real life)

Same file here too.

In tmp/heroes_list:

id:11,name:Mr. Nice
id:12,name:Narco
id:13,name:Bombasto
id:14,name:Celeritas
id:15,name:Magneta
id:16,name:RubberMan
id:17,name:Dynama
id:18,name:Dr IQ
id:19,name:Magma
id:20,name:Tornado

And that’s all for the rails configuration. Let’s do the Alt and React one now…

2 – Alt and React configuration

Files tree:
 
(app/assets)
|
|_ (javascripts)
| |_ application.js
| |_ components.js
| |_ alt_init.js
| |_ routes.es6.jsx
| |
| |_ (actions)
| | |_ HeroActions.es6
| |
| |_ (stores)
| | |_ HeroStore.es6
| |
| |_ (components)
| | |_ app-component.es6.jsx
| | |_ dashboard-component.es6.jsx
| | |_ heroes-component.es6.jsx
| | |_ hero-detail-component.es6.jsx
|
|_ (stylesheets)
| |_ style.css
| |_ application.css
| |_ app.component.css
| |_ dashboard.component.css
| |_ hero-detail.component.css

2.1 – javascripts configuration

2.1.1 – Update application.js

We must require the React, ReactRouter.min, and Alt libraries. The React library allows us to use React components, the ReactRouter to manage the routing on the client side, and the Alt library to add some Flux architecture to our project.

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs

//= require react
//= require react_ujs
//= require ReactRouter.min
//= require alt
//= require components

2.1.2 – Update components.js

components.js contains all the files used by React. As we are using Flux architecture, we decompose our project in « actions », « stores » and components.

In app/assets/javascripts/components.js:

//= require routes
//= require alt_init
//= require_tree ./actions
//= require_tree ./stores
//= require_tree ./components

2.1.3 – Alt Configuration

  • Create « alt_init.js » file. This file initializes the Alt object that will be used to bring Flux architecture to the project.

In app/assets/javascripts/alt_init.js:

var alt = new Alt();
  • Create the « actions » directory and the « HeroActions.es6 » file inside (note that the extension is .es6 so that the sprockets-es6 gem can recognize it and transforms it).

In app/assets/javascripts/actions/HeroActions.es6:

class HeroActions {
  
  constructor() {
    //the following line enables to create actions that only do a 'return' of the parameter
    this.generateActions('updateHeroes', 'updateHeroDetail', 'updateHeroName', 'selectHero');
  }
  
  fetchHeroes () {
     return (dispatch) => {
      // we dispatch an event here so we render a specific div while loading (manage by onFetchHeroes in HeroStore).
      dispatch();
      $.get('/heroes', function (results) { return results } )
        .done((heroes) => { this.updateHeroes(heroes) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  fetchHeroDetail (id) {
    return (dispatch) => {
      // we dispatch an event here so we render a specific div while loading (manage by onFetchHeroDetail in HeroStore).
      dispatch();
      $.get('/heroes/' + id, function (result) { return result } )
        .done((hero) => { this.updateHeroDetail(hero) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  updateHeroNameServer (id,newname) {
    return (dispatch) => {
       $.ajax( { url: '/heroes/' + id, type: 'put', data: {id: id, name: newname} } )
        .done(() => { } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  heroesFailed (errorMessage) {
    return errorMessage;
  }
  
}

var heroActions = alt.createActions(HeroActions);

  • Create stores directory and create the HeroStore.es6 file.

In app/assets/javascripts/stores/HeroStore.es6:

class HeroStore {

  constructor () {
    this.heroes = [];
    this.hero = null;
    this.heroSelected = null;
    this.errorMessage = null;
    
    this.bindActions(heroActions);
  }
  
  onUpdateHeroes (heroes) {
    this.heroes = heroes;
  }
  
  onUpdateHeroDetail (hero) {
    this.hero = hero;
  }
  
  onUpdateHeroName (name) {
    this.hero.name = name;
  }
  
  onSelectHero (hero) {
    this.heroSelected = hero;
  }
  
  onFetchHeroes () {
    // reset the array while we're fetching new heroes so React can
    // be smart and render a specific div for us since the data is empty.
    this.heroes = [];
  }
  
  onFetchHeroDetail () {
    // reset the array while we're fetching new heroes so React can
    // be smart and render a specific div for us since the data is empty.
    this.hero = null;
  }
  
  onHeroesFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }

}

var heroStore = alt.createStore(HeroStore, 'HeroStore');

2.1.4 – ReactRouter configuration

Create the « routes.es6.jsx » file. It is this file that will manage routing on client side.

In app/assets/javascripts/routes.es6.jsx:

var ReactRouter = window.ReactRouter
var Router = ReactRouter.Router
var Route = ReactRouter.Route
var Link = ReactRouter.Link
var Redirect = ReactRouter.Redirect
var browserHistory = ReactRouter.browserHistory

class StaticRoute extends React.Component {
  render () {
    return (
        <Router history={browserHistory}>
          <Redirect from="/" to="/dashboard" />
          <Route path="/" component={AppComponent}>            
            <Route path="/dashboard" component={DashboardComponent}></Route>
            <Route path="/heroes-list" component={HeroesComponent}></Route>
            <Route path="/hero-detail/:id" component={HeroDetailComponent} />
          </Route>
        </Router>
      );
  }
}

2.1.5 – React configuration

Create the « components » directory and the following files: « app-component.es6.jsx », « dashboard-component.es6.jsx », « heroes-component.es6.jsx », « hero-detail-component.es6.jsx ».

In app/assets/javascripts/application/components/app-component.es6.jsx:

class AppComponent extends React.Component {
  render () {
    var title = 'Tour of Heroes'

    return (
      <div>
        <h1>{title}</h1>
        <nav>
           <Link to='/heroes-list'>Heroes</Link>
           <Link to='/dashboard'>Dashboard</Link>
        </nav>
        {this.props.children}
      </div>
    )
  }
}

In app/assets/javascripts/application/components/dashboard-component.es6.jsx:

class DashboardComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this); //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }

  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (!this.state.heroes.length) {
      return (
        <div>Loading dashboard...</div>
      );
    }
    else {
      return (
        <div>
          <h3>Top Heroes</h3>
          <div className='grid grid-pad'>          
            { this.state.heroes.slice(0,4).map(
              hero => {
                return (
                  <div key={hero.id} className='col-1-4' >
                    <div className='module hero' onClick={this.handleDetailClick.bind(this,hero.id)}><h4>{hero.name}</h4></div>
                  </div>    
                )
              }
            )}
          </div>
        </div>      
      )
    }
  }
}

In app/assets/javascripts/application/components/heroes-component.es6.jsx:

class HeroesComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this); //allows the use of 'this' in onChange
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroes();
    heroActions.selectHero(null);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }

  handleClick (hero) {
    heroActions.selectHero(hero);
  }
  handleDetailClick (id) {
    browserHistory.push('/hero-detail/' + id);
  }
  
  render () {
    return (
    <div>
      <h2>My Heroes</h2>
      <ul className='heroes'>      
        { this.state.heroes.map(
          hero => {
            var liClass = '';
            if(this.state.heroSelected && (this.state.heroSelected == hero)) { liClass = 'selected' };
            return (
              <li key={hero.id} className={liClass} onClick={this.handleClick.bind(this,hero)}>
                <span className='badge'>{hero.id}</span> {hero.name}
              </li>
            )
          }
        )}
      </ul>
      { ( () => {
          if(this.state.heroSelected) {
            return (
              <div>
                <h2>{this.state.heroSelected.name.toUpperCase()} is my hero</h2>
                <button onClick={this.handleDetailClick.bind(this,this.state.heroSelected.id)}>View Details</button>
              </div>
            )
          }
        } )()
      }
    </div>
    )
  }
}

In app/assets/javascripts/application/components/hero-detail-component.es6.jsx:

class HeroDetailComponent extends React.Component {
  constructor () {
    super();
    this.state = heroStore.getState();
    this.onChange = this.onChange.bind(this);
    this.updateHeroClick = this.updateHeroClick.bind(this);
  }
  onChange(state) {
    this.setState(state);
  }
  componentDidMount () {
    heroStore.listen(this.onChange);
    heroActions.fetchHeroDetail(this.props.params.id);
  }
  componentWillUnmount() {
    heroStore.unlisten(this.onChange);
  }
  
  handleChange (e) {
    name = e.target.value;
    heroActions.updateHeroName(name);
  }
  goBackClick (id) {
    window.history.back();
  }
  updateHeroClick () {
    heroActions.updateHeroNameServer(this.state.hero.id,this.state.hero.name);
    alert("updated!");
  }
  
  render () {
    if (this.state.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (this.state.hero == null) {
      return (
        <div>Loading hero...</div>
      );
    }
    else {
      return (
        <div>
          <h2>{this.state.hero.name} details</h2>
          <div>
            <label>id: </label>{this.state.hero.id}
          </div>
          <div>
            <label>name: </label>
            <input type="text" value={this.state.hero.name} placeholder={this.state.hero.name} onChange={this.handleChange} />
          </div>
          <button onClick={this.goBackClick}>Back</button>
          <button onClick={this.updateHeroClick}>Update</button>
        </div>
      )
    }
  }
}

2.2 – stylesheets configuration

Exactly the same stylesheets configuration as with our Angular2 project.

2.2.1 – Update application.css if necessary

In app/assets/stylesheets/application.css:

/*
 *= require_tree .
 *= require_self
 */

2.2.2 – Create the following files

In app/assets/stylesheets/style.css:

h2 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: #888;
  font-family: Cambria, Georgia;
}
button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

In app/assets/stylesheets/app.component.css:

h1 {
  font-size: 1.2em;
  color: #999;
  margin-bottom: 0;
}
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #eee;
  border-radius: 4px;
}
nav a:visited, a:link {
  color: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.router-link-active {
  color: #039be5;
}

In app/assets/stylesheets/dashboard.component.css:

.selected {
  background-color: #CFD8DC !important;
  color: white;
}

.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 10em;
}

.heroes li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: .3em 0;
  height: 2em;
  border-radius: 4px;
}

.heroes li.selected:hover {
  background-color: #BBD8DC !important;
  color: white;
}

.heroes li:hover {
  color: #607D8B;
  background-color: #DDD;
  left: .1em;
}

.heroes .text {
  position: relative;
  top: -3px;
}

.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #607D8B;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 2.4em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}

[class*='col-'] {
  float: left;
}
*, *:after, *:before {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
h3 {
  text-align: center; margin-bottom: 0;
}
[class*='col-'] {
  padding-right: 20px;
  padding-bottom: 20px;
}
[class*='col-']:last-of-type {
  padding-right: 0;
}
.grid {
  margin: 0;
}
.col-1-4 {
  width: 25%;
}
.module {
    padding: 20px;
    text-align: center;
    color: #eee;
    max-height: 120px;
    min-width: 120px;
    background-color: #607D8B;
    border-radius: 2px;
}
h4 {
  position: relative;
}
.module:hover {
  background-color: #EEE;
  cursor: pointer;
  color: #607d8b;
}
.grid-pad {
  padding: 10px 0;
}
.grid-pad [class*='col-']:last-of-type {
  padding-right: 20px;
}
@media (max-width: 600px) {
    .module {
      font-size: 10px;
      max-height: 75px; }
}
@media (max-width: 1024px) {
    .grid {
      margin: 0;
    }
    .module {
      min-width: 60px;
    }
}

In app/assets/stylesheets/hero-detail.component.css:

label {
  display: inline-block;
  width: 3em;
  margin: .5em 0;
  color: #607D8B;
  font-weight: bold;
}
input {
  height: 2em;
  font-size: 1em;
  padding-left: .4em;
}
button {
  margin-top: 20px;
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer; cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #ccc;
  cursor: auto;
}

3 – Start the server and see the results

rails s

And with your favorite browser go to: http://your-server

That’s all for rails with React and Alt in ES6…

Publicités

4 réflexions sur “Rails and React and Alt « Tour of Heroes » tutorial

  1. This is a great article – it got me going with Rails 5, React & Alt. I found it very useful to look at the ES6 syntax for a simple working example and was able to advance it from there. Thanks for writing this up, good work!

    J'aime

    • Hi James,
      Thanks for your message. I am happy to see that my work can help.
      I hesitated at first to spend some time learning ES6, but in fact it goes pretty well along with React, the syntax being a little cleaner and simpler than with plain javascript.

      By the way, do not hesitate to have a look at my post about performances (or the full tutorial post), because using wisely Immutable and pure components can be very powerful. I also deeply encourage using containers as it allows to structure your code and to separate efficiently your « data fetching » and your « rendering ».

      J'aime

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s