Rails and React – Full tutorial

1 – Introduction

Ok, now that I have been studying React environment for the past few months, I think that it is the right time to present a complete example of what we can do, and how to do it cleanly.

1.1 – The « what »

Right below is what we are going to do. It is completely inspired from « The Tour of Heroes » example created by the Angular2 team, so all credits for the idea go to them. It is a simple example with a public home page, and a private part displaying a list of video games and the list of the best ones (if you do not agree with the ranking, let me know 😉 ):

The home page

It is the only public page of our application. This page will be prerendered (rendered on server side) so that it will generate HTML, and therefore will be easier to be crawled by search engines (Google seems to be pretty good at crawling javascript, but we have to care about the other ones).

home_page

The login page

If you click on the « Video Games » button on the page before, you will be asked to log in. This page is managed by Devise (rails gem). If you are already logged in, you will automatically be redirected to the list of the best video games. I did not take the time to make a link to the « sign up » page, but you can get to it by changing « sign_in » with « sign_up » in the url.

login_page

Best Video Games list page

You will see a title « Video Games », 2 links and a list of 4 video games (and an image). The part with the title and the links will be on all your private pages. If you click on « Video Games List » you will get to the list of all the video games (and if you click on « Best Video Games » you will go back to this page). If you click on one of the video game, you will get to the « Video Game Detail » page.

best_videogames_page

Video games list page

Here you will see the list of all the video games recorded in your database.

videogame_list_page

If you click on one of them, the name will be displayed below, with a link to the « Video Game Detail » page.

videogame_selected_page

Video game detail page

This page will show the id and name of the selected video game (either from the best video games list or the full video games list). If you modify the name in the text field, it will automatically and instantly modify the title. If you click on the « Update » button, it will modify the name in the database, and display a pop-up saying « updated! ».

videogame_detail_page

1.2 – The « how »

And here are the technologies that we are going to use to do it:

  • Ruby on Rails (and Devise)
  • React (with react-rails gem)
  • React-router
  • Alt
  • Containers (with Alt Container)
  • Immutable (and PureRenderMixin)

We do not use npm so that our project remains light and easy (I have had some bad experience with npm, which downloaded 500Mb of node modules before I even started to write a single line of code).

Ruby on Rails

No need to speak a lot about it. It is a mature and efficient technology to develop Web sites, which helps you to do it fast and clean.

You may want to read more here: http://rubyonrails.org/

React

It is a javascript library built by Facebook team, to manage the views (and only the views!) of your website. It displays the DOM very quickly and enables to split up your code to improve visibility and maintenance.

The official website: https://facebook.github.io/react/

We are going to use the great react-rails gem (rails library), which contains everything we need: https://github.com/reactjs/react-rails

React-Router

React-Router enables you to handle routing on client side. Even though Rails has a great routing system, I found it pretty useful to manage all my private links with react-router. One of the benefit is that you spare some server requests as your client do not ask the server for every route.

Official website: https://github.com/reactjs/react-router

Alt

Alt is a flux compliant library. If you do not know why to use a flux architecture, you may want to take a look here: https://facebook.github.io/flux/docs/overview.html. I had a quick view at different flux implementations, and Alt was exactly what I was looking for: light, easy to use, with a good documentation and community, and with a lot of automatic mechanisms.

Do not hesitate to visit the official website: http://alt.js.org/

Containers

Containers are very important to structure your code, and force you to develop cleanly. I found this article quite interesting: https://medium.com/@learnreact/container-components-c0e67432e005#.oiysew4cr

We are going to use Alt containers as they offer a few tricks to lighten our code.

Immutable

Immutable enables to compare deeply and easily two Javascript Objects. It will be very useful to improve performance, as it will enable to render React components only if required (thanks to the PureRenderMixin React add on).

You can find the official documentation of Immutable here: https://facebook.github.io/immutable-js/, and you can read an explanation of the functioning in one of my previous post: React and Immutable.js

 

We are now ready, so let’s start…

 

1 – Server configuration (Rails)

Modified files tree:
/
|
|_config
| |
| |_environments
| | |
| | |_development.rb
| | |
| | |_production.rb
| |
| |_applications.rb
| |
| |_routes.rb
| |
| |_database.yml
|
|_db
| |
| |_migrate
| | |
| | |_xxxxxxxxxxxx_create_video_games.rb
| |
| |_seeds.rb
|
|_app
| |
| |_controllers
| | |
| | |_home_controller.rb
| | |
| | |_private_controller.rb
| | |
| | |_video_games_controller.rb
|
|_vendor
| |
| |_assets
| | |
| | |_javascripts
| | | |
| | | |_ReactRouter.min.js
| | | |
| | | |_alt.js
| | | |
| | | |_alt-container.js
| | | |
| | | |_immutable.min.js
|
|_Gemfile

1.1 – Create a new rails project

rails new react_full_example

1.2 – Update Gemfile

We need a javascript runtime (to manage javascript), so we are going to use « therubyracer« . I have seen different point of view whether nodejs or therubyracer is better, but it seems that therubyracer is now more efficient (if you prefer nodejs, do not uncomment the following line, and install nodejs with: apt-get install nodejs). For therubyracer, we uncomment this line in our Gemfile:

gem 'therubyracer', platforms: :ruby

We are going to use mysql database. The installation of mysql is out of this scope. We will consider that mysql has already been installed, and that a database named « react_tests » was created with user/password equal to react_test/react_test. Now we add the following line in our Gemfile:

gem 'mysql2'

We are going to have a « private » part, that is to say a part which requires authentication. As authentication mechanism I suggest we use devise. Let’s add it in our Gemfile:

gem 'devise'

To include React js in our project, there is a great gem called react-rails. It contains everything we need such as the React library, the addons libraries, and a .jsx transformer (currently Babel). So, we add this line:

gem 'react-rails'

Finally, we are going to use an es6 transpiler as we want all our javascript code to be in ES6 langage. So we add this line:

gem 'sprockets-es6'

Here is how the Gemfile should look like:

/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'
gem 'mysql2'
# 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'

gem 'devise'

Do not forget to run:

bundle install

1.3 – Generate gem files

Now, we have to run the following commands, to create the files associated to devise and react gems. You can check official documentations if necessary to understand what it is doing exactly:

rails generate devise:install
rails generate devise User
rails g react:install

1.4 – Configure the gems (mysql and React)

mysql

The configuration is made in database.yml file. Below is an example, but adapt it to fit your own database configuration.

/config/database.yml

development:
  adapter: mysql2
  encoding: utf8
  collation: utf8_general_ci
  database: react_tests
  host: 127.0.0.1
  pool: 5
  username: react_test
  password: react_test

react

First, we modify application.rb by adding config.react.server_renderer_options  and config.react.addons. The first variable enables to configure server rendering, and the second one activates React add ons (in our case we are mainly interested by the PureRenderMixin add on).

/config/application.rb

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ReactJSWithReactRails
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    # Do not swallow errors in after_commit/after_rollback callbacks.
    config.active_record.raise_in_transactional_callbacks = true
    
    config.react.server_renderer_options = {
      files: ["react-server.js", "alt_init.js", "routes_init.js", "prerender.js"], # files to load for prerendering
      replay_console: true,                 # if true, console.* will be replayed client-side
    }
    
    #reactjs
    config.react.addons = true
  end
end

Then we only add a line (config.react.variant initialization), in the two following files to optimize the react library depending whether we are on development or production environment.

/config/environments/development.rb

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded on
  # every request. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.cache_classes = false

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports and disable caching.
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Debug mode disables concatenation and preprocessing of assets.
  # This option may cause significant delays in view rendering with a large
  # number of complex assets.
  config.assets.debug = true

  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
  # yet still be able to expire them through the digest params.
  config.assets.digest = true

  # Adds additional error checking when serving assets at runtime.
  # Checks for improperly declared sprockets dependencies.
  # Raises helpful error messages.
  config.assets.raise_runtime_errors = true

  # Raises error for missing translations
  # config.action_view.raise_on_missing_translations = true
  
    
  #to hide the following error message: "Cannot render console from 31.34.231.93! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255"
  config.web_console.whiny_requests = false
  
  #reactjs
  config.react.variant = :development  
end

/config/environments/production.rb

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.cache_classes = true

  # Eager load code on boot. This eager loads most of Rails and
  # your application in memory, allowing both threaded web servers
  # and those relying on copy on write to perform better.
  # Rake tasks automatically ignore this option for performance.
  config.eager_load = true

  # Full error reports are disabled and caching is turned on.
  config.consider_all_requests_local       = false
  config.action_controller.perform_caching = true

  # Enable Rack::Cache to put a simple HTTP cache in front of your application
  # Add `rack-cache` to your Gemfile before enabling this.
  # For large-scale production use, consider using a caching reverse proxy like
  # NGINX, varnish or squid.
  # config.action_dispatch.rack_cache = true

  # Disable serving static files from the `/public` folder by default since
  # Apache or NGINX already handles this.
  config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?

  # Compress JavaScripts and CSS.
  config.assets.js_compressor = :uglifier
  # config.assets.css_compressor = :sass

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = false

  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
  # yet still be able to expire them through the digest params.
  config.assets.digest = true

  # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb

  # Specifies the header that your server uses for sending files.
  # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
  # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  # config.force_ssl = true

  # Use the lowest log level to ensure availability of diagnostic information
  # when problems arise.
  config.log_level = :debug

  # Prepend all log lines with the following tags.
  # config.log_tags = [ :subdomain, :uuid ]

  # Use a different logger for distributed setups.
  # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)

  # Use a different cache store in production.
  # config.cache_store = :mem_cache_store

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  # config.action_controller.asset_host = 'http://assets.example.com'

  # Ignore bad email addresses and do not raise email delivery errors.
  # Set this to true and configure the email server for immediate delivery to raise delivery errors.
  # config.action_mailer.raise_delivery_errors = false

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation cannot be found).
  config.i18n.fallbacks = true

  # Send deprecation notices to registered listeners.
  config.active_support.deprecation = :notify

  # Use default logging formatter so that PID and timestamp are not suppressed.
  config.log_formatter = ::Logger::Formatter.new

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false
  
  #reactjs
  config.react.variant = :production
end

1.5 – Get ReactRouter, Alt and Immutable Javascript Libraries

ReactRouter

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

Alt

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

Alt-container

For this one, I have not been able to find a plain javascript library. So I took an ES6 version here: https://github.com/altjs/container/tree/master/src, and transformed it in a plain javascript file so that I could put in my vendor directory (I could probably have used the ES6 version but it does not work in vendor directories and I did not want to mix it with my other javascript files).

/vendor/assets/javascripts/alt-container.js

'use strict';

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }


var object_assign = function( O, dictionary ) {
  var target, src;

  // Let target be ToObject(O).
  target = Object( O );

  // Let src be ToObject(dictionary).
  src = Object( dictionary );

  // For each own property of src, let key be the property key
  // and desc be the property descriptor of the property.
  Object.getOwnPropertyNames( src ).forEach(function( key ) {
    target[ key ] = src[ key ];
  });

  return target;
};

var id = function id(it) {
  return it;
};
var getStateFromStore = function getStateFromStore(store, props) {
  return typeof store === 'function' ? store(props).value : store.getState();
};
var getStateFromKey = function getStateFromKey(actions, props) {
  return typeof actions === 'function' ? actions(props) : actions;
};

var getStateFromActions = function getStateFromActions(props) {
  if (props.actions) {
    return getStateFromKey(props.actions, props);
  } else {
    return {};
  }
};

var getInjected = function getInjected(props) {
  if (props.inject) {
    return Object.keys(props.inject).reduce(function (obj, key) {
      obj[key] = getStateFromKey(props.inject[key], props);
      return obj;
    }, {});
  } else {
    return {};
  }
};

var reduceState = function reduceState(props) {
  return object_assign({}, getStateFromStores(props), getStateFromActions(props), getInjected(props));
};

var getStateFromStores = function getStateFromStores(props) {
  var stores = props.stores;
  if (props.store) {
    return getStateFromStore(props.store, props);
  } else if (props.stores) {
    // If you pass in an array of stores then we are just listening to them
    // it should be an object then the state is added to the key specified
    if (!Array.isArray(stores)) {
      return Object.keys(stores).reduce(function (obj, key) {
        obj[key] = getStateFromStore(stores[key], props);
        return obj;
      }, {});
    }
  } else {
    return {};
  }
};

// TODO need to copy some other contextTypes maybe?
// what about propTypes?

var AltContainer = (function (_React$Component) {
  _inherits(AltContainer, _React$Component);
  
  AltContainer.contextTypes = {
    flux: React.PropTypes.object,
  }

  AltContainer.childContextTypes =  {
    flux: React.PropTypes.object,
  }

  _createClass(AltContainer, [{
    key: 'getChildContext',
    value: function getChildContext() {
      var flux = this.props.flux || this.context.flux;
      return flux ? { flux: flux } : {};
    }
  }]);

  function AltContainer(props) {
    _classCallCheck(this, AltContainer);

    _get(Object.getPrototypeOf(AltContainer.prototype), 'constructor', this).call(this, props);

    if (props.stores && props.store) {
      throw new ReferenceError('Cannot define both store and stores');
    }

    this.storeListeners = [];

    this.state = reduceState(props);
  }

  _createClass(AltContainer, [{
    key: 'componentWillReceiveProps',
    value: function componentWillReceiveProps(nextProps) {
      this._destroySubscriptions();
      this.setState(reduceState(nextProps));
      this._registerStores(nextProps);
      if (this.props.onWillReceiveProps) {
        this.props.onWillReceiveProps(nextProps, this.props, this.context);
      }
    }
  }, {
    key: 'componentDidMount',
    value: function componentDidMount() {
      this._registerStores(this.props);
      if (this.props.onMount) this.props.onMount(this.props, this.context);
    }
  }, {
    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      this._destroySubscriptions();
      if (this.props.onWillUnmount) {
        this.props.onWillUnmount(this.props, this.context);
      }
    }
  }, {
    key: '_registerStores',
    value: function _registerStores(props) {
      var _this = this;

      var stores = props.stores;

      if (props.store) {
        this._addSubscription(props.store);
      } else if (props.stores) {
        if (Array.isArray(stores)) {
          stores.forEach(function (store) {
            return _this._addSubscription(store);
          });
        } else {
          Object.keys(stores).forEach(function (formatter) {
            _this._addSubscription(stores[formatter]);
          });
        }
      }
    }
  }, {
    key: '_destroySubscriptions',
    value: function _destroySubscriptions() {
      this.storeListeners.forEach(function (storeListener) {
        return storeListener();
      });
    }
  }, {
    key: '_addSubscription',
    value: function _addSubscription(getStore) {
      var store = typeof getStore === 'function' ? getStore(this.props).store : getStore;

      this.storeListeners.push(store.listen(this.altSetState.bind(this)));
    }
  }, {
    key: 'altSetState',
    value: function altSetState() {
      this.setState(reduceState(this.props));
    }
  }, {
    key: 'getProps',
    value: function getProps() {
      var flux = this.props.flux || this.context.flux;
      var transform = typeof this.props.transform === 'function' ? this.props.transform : id;
      return transform(object_assign(flux ? { flux: flux } : {}, this.state));
    }
  }, {
    key: 'shouldComponentUpdate',
    value: function shouldComponentUpdate(nextProps, nextState) {
      return this.props.shouldComponentUpdate ? this.props.shouldComponentUpdate(this.getProps(), nextProps, nextState) : true;
    }
  }, {
    key: 'render',
    value: function render() {
      var _this2 = this;

      var Node = 'div';
      var children = this.props.children;

      // Custom rendering function
      if (typeof this.props.render === 'function') {
        return this.props.render(this.getProps());
      } else if (this.props.component) {
        return React.createElement(this.props.component, this.getProps());
      }

      // Does not wrap child in a div if we don't have to.
      if (Array.isArray(children)) {
        return React.createElement(Node, null, children.map(function (child, i) {
          return React.cloneElement(child, object_assign({ key: i }, _this2.getProps()));
        }));
      } else if (children) {
        return React.cloneElement(children, this.getProps());
      } else {
        return React.createElement(Node, this.getProps());
      }
    }
  }]);

  return AltContainer;
})(React.Component);

Note: For those who do not want to use alt-container, I still recommend to use the « containers » structure because it helps coding cleanly, but I will show them how to do it without the « alt-container.js » file: the code will be commented at the end of each « container » file.

Immutable

Finally, we download the Immutable javascript file and put it in our vendor directory. The immutable documentation « Getting started » page gives you two links, I took this one: https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js.

get https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js file and put it there: vendor/assets/javascripts/immutable.min.js

1.6 – Create Controllers

An important advise that can save you a lot of troubles: always keep your controllers light! Export all you can in models or helpers so that you will be able to reuse the code if necessary. It follows one of Rails principle: to have a DRY code (see here for more information). It does not really apply to our case as we do very little here, but it is good to always keep that in mind.

home_controller

This controller manages our home page. As the home page is public, we want it to be rendered on server side, so that html is given back to the browser.

/app/controllers/home_controller.rb

class HomeController < ApplicationController

  def index
    render inline: "<%= react_component('HomeComponent', {}, {prerender: true}) %>", layout: 'application'
  end

end

private_controller

The private_controller handles all private pages. Therefore we ask it to render a ReactRouter component called « PrivateRoute » that will manage all the other private components.

/app/controllers/private_controller.rb

class PrivateController < ApplicationController
  before_action :authenticate_user!

  def index
    render inline: "<%= react_component('PrivateRoute') %>", layout: 'application'
  end

end

Notice the « before_action :authenticate_user! » at the top of the file: it is a devise method that checks if the user is authenticated, and if it is not the case it redirects to the authentication page.

video_games_controller

This controller manages the « video games » object with CRUD operations.

/app/controllers/video_games_controller.rb

class VideoGamesController < ApplicationController
  before_action :authenticate_user!

  def index
    respond_to do |format|
      format.json { render json: VideoGame.all.order(:id) }
    end
  end
  
  def show
    respond_to do |format|
      format.json { render json: VideoGame.find(params[:id]) }
    end
  end
  
  def update
    videogame = VideoGame.find(params[:id])
    videogame.update(name: params[:name])
      
    respond_to do |format|
      format.json { render json: videogame }
    end
  end

end

Here too we have the « before_action :authenticate_user! » method as we do not want non authenticated users to be able to call these actions.

1.7 – Update the routes

In our Rails routes file, we have 4 different parts: the usual « root » line, the « devise » routes (managed with « devise_for » command), one route for each controller (managed with « ressources » command), and finally 2 routes (managed with a « match » command).

/config/routes.rb

Rails.application.routes.draw do
  root 'home#index'
  
  devise_for :users

  resources :video_games, only: [:index, :show, :update]
  resources :home, only: :index
  resources :private, only: :index

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

The 3 first parts are quite classical, but let’s stop a little while on the last one. As we are using ReactRouter, most routing will be handled on client side, which means that your browser will know, for example, how to  go from http://<your server>/private/video-game-list to http://<your server>/private/video-game-best without asking the server. However, your server will not be able to understand these URLs! Thus, if you put the URL directly into your navigation bar, the request will be sent to the server who will return an error. Therefore, the last part enables to manage routes that are sent directly to the server (which is useful for all external links).

1.8 – Create and fill the database

Last thing to do on server side: prepare the database.

Create Video Games table

Lets create a Video Games table in our database:

rails generate migration CreateVideoGames

We fill it with this content:

/db/migrate/xxxxxxxxx_create_video_games.rb

class CreateVideoGames < ActiveRecord::Migration
  def change
    create_table :video_games do |t|
      t.string   :name
    end
    
    add_index :video_games, :id, unique: true
    add_index :video_games, :name
  end
end

And run:

rake db:migrate

Seed data into Video Games table

Now we are going to « seed » a few data in it.

Modify your seeds.rb file:

/db/seeds.rb

[{ id: 1, name: "Monkey Island"}, { id: 2, name: "Fallout2"}, { id: 3, name: "Prince of Persia"}, 
  { id: 4, name: "Flashback"}, { id: 5, name: "MaxPayne2"}, { id: 6, name: "Dune"}, 
  { id: 7, name: "Lemmings"}, { id: 8, name: "Ultima 7"}, { id: 9, name: "Sherman M4"}, 
  { id: 10, name: "Tetris"}].each do |videogame|
  VideoGame.create(videogame)
end

and run:

rake db:seed

 

We are done with the server part, let’s do (what I call) the Client configuration…

 

2 – Client configuration (React)

Modified files tree:

/
|
|_app
| |
| |_assets
| | |
| | |_javascripts
| | | |
| | | |_prerender
| | | | |
| | | | |_home-component.jsx.erb
| | | |
| | | |_actions
| | | | |
| | | | |_VideoGameActions.es6
| | | |
| | | |_sources
| | | | |
| | | | |_VideoGameSource.es6
| | | |
| | | |_stores
| | | | |
| | | | |_VideoGameStore.es6
| | | |
| | | |_containers
| | | | |
| | | | |_videogame-best-container.jsx.erb
| | | | |
| | | | |_videogame-list-container.jsx.erb
| | | | |
| | | | |_videogame-detail-container.jsx.erb
| | | |
| | | |_components
| | | | |
| | | | |_app-component.jsx.erb
| | | | |
| | | | |_videogame-best-component.jsx.erb
| | | | |
| | | | |_videogame-list-component.jsx.erb
| | | | |
| | | | |_videogame-detail-component.jsx.erb
| | | | |
| | | | |_videogame-selected-component.jsx.erb
| | | |
| | | |_applications.js
| | | |
| | | |_prerender.js
| | | |
| | | |_components.js
| | | |
| | | |_alt_init.js
| | | |
| | | |_addons_init.js
| | | |
| | | |_routes_init.js
| | | |
| | | |_routes.jsx
| | |
| | |_stylesheets
| | | |
| | | |_style.css
| | | |
| | | |_app.component.css
| | | |
| | | |_video-game-best.component.css
| | | |
| | | |_video-game-detail.component.css
| | |
| | |_images
| | | |
| | | |_my-image.png

2.1 – Javascripts configuration

2.1.1 – Initialization scripts

Update applications.js

In application.js, we add the react and immutable libraries, as well as the initialization files that we are going create right away. We also add the « routes » file, which will manage routing on client side, and the « components » file, which will include all react files.

/app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs

//= require react
//= require react_ujs

//= require immutable.min

//= require alt_init
//= require routes_init
//= require addons_init

//= require routes
//= require components

Create alt_init.js

In alt.js, we include alt and alt-container libraries, and initialize Alt.

/app/assets/javascripts/alt_init.js

//= require alt
//= require alt-container

var alt = new Alt();

Create addons_init.js

In this file we initialize PureRenderMixin, which will enable us to improve performances in React components.

/app/assets/javascripts/addons_int.js

var PureRenderMixin = React.addons.PureRenderMixin;

Configure routes_init.js

In routes_init.js, we initialize all variables that we will need to manage our routes.

/app/assets/javascripts/routes_init.js

//= require ReactRouter.min

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

Create components.js

Here, we include all React files, that is to say:

  • files that will be prerendered on server side (included in prerender.js file)
  • actions files
  • data sources files
  • stores files
  • containers files
  • components files

/app/assets/javascripts/components.js

//= require prerender
//= require_tree ./actions
//= require_tree ./sources
//= require_tree ./stores
//= require_tree ./containers
//= require_tree ./components

Create prerender.js

We only include all files that will be created in prerender directory. These files are the one that will be rendered on server side, to generate HTML, so that it is better crawled by search engine.

/app/assets/javascripts/prerender.js

/* 
This file is used in application.rb for prerendering react on server side.
It must also be included in components.js otherwise we get an error on client side.
*/
//= require_tree ./prerender

2.1.2 – Application scripts

Create routes.jsx

routes.jsx file contains the PrivateRoute component, which is, as you may recall, the component that is called in private_controller.rb (index action). It is also called in any case an URL starting with « private » is sent to the server (as described by « match private/*path » in routes.rb).

/app/assets/javascripts/routes.jsx

class PrivateRoute extends React.Component {
  render () {
    return (
        <Router history={browserHistory}>
          <Redirect from="/private" to="/private/video-game-best" />
          <Route path="/private" component={AppComponent}>
            <Route path="/private/video-game-list" component={VideoGameListContainer}></Route>
            <Route path="/private/video-game-best" component={VideoGameBestContainer}></Route>            
            <Route path="/private/video-game-detail/:id" component={VideoGameDetailContainer} />
          </Route>
        </Router>
      );
  }
}

A brief explanation:

  • in Router element, browserHistory enables the « browserHistory.push » function that tells the browser to go on a specific URL
  • the Redirect element redirects « /private » url to « /private/video-game-best ».
  • finally, the tree tells the browser to display the AppComponent at the top, and one of the following container below: VideoGameListContainer, VideoGameBestContainer, VideoGameDetailContainer.

prerender directory

Create /app/assets/javascripts/prerender directory, and create a home-component.jsx.erb file in it, with the following content:

/app/assets/javascripts/prerender/home-component.jsx.erb

class HomeComponent extends React.Component {
  render () {  
    return (
      <div>
        <h3>Home page</h3>
        <div>
          This is the home page!
          
          <nav>
            <a href='/private/video-game-best'>Video Games</a>
          </nav>
        </div>
      </div>      
    )
  }
}

This component is our home page. It contains only one link redirecting to the private part.

actions directory

Now, create /app/assets/javascripts/prerender directory, with a VideoGameActions.es6 file in it (the « es6 » extension enables the file to be translated by « sprockets-es6 » gem). Every time a React component needs to do an action on Video Games, it will go through this file.

/app/assets/javascripts/prerender/VideoGameActions.es6

class VideoGameActions {
  
  constructor() {
    //the following line enables to create actions that only 'return' parameters
    this.generateActions('updateVideoGames', 'updateVideoGameDetail', 'updateVideoGameName', 'selectVideoGame', 'updateVideoGameNameServer', 'videoGameFailed', 'videoGameLoading');
  }

}

var videoGameActions = alt.createActions(VideoGameActions);

Note: we can do a lot of operations in this file, for example fetching data from the server, but I prefer to do the fetching in the « data source » file. Therefore, here, we only return parameters to the store, so we can use the generateActions Alt function (it keeps us from creating a method for each action).

A brief description of each action:

  • updateVideoGames: this action updates the « video games list » and the « best video games list ». It is called when displaying the « video games list » page and the « best video games list »page.
  • updateVideoGameDetail: this action updates the video game details. It is called when displaying the video game details page.
  • updateVideoGameName: this action updates the video game name in the video game details page. It is called every time a key is typed in the video game name field, and it updates instantly the video game name displayed above.
  • selectVideoGame: this action displays a block with the selected video game below the list of video games. It is called when the user clicks on a video game in the video games list.
  • updateVideoGameNameServer: this actions updates the name of the video game in the database. It is called when the user clicks on the « update » button on the video game details page.
  • videoGameFailed: this action is called if there is a failure when fetching data (the list of video games, the detail of a video game) from the server or updating data (the name of the video game) on the server.
  • videoGameLoading: this action is called when fetching the video game detail from the server. It allows to display a « loading div » while the data is being fetch.

sources directory (data source)

Create /app/assets/javascripts/sources directory, and create a VideoGameSource.es6 file in it, with the following content:

/app/assets/javascripts/sources/VideoGameSource.es6

const VideoGameSource = {
  
  fetchVideoGames: {
    // should fetch has precedence over the value returned by local in determining whether remote should be called
    // in this particular example if the value is present locally it would return but still fire off the remote request (optional)
    //shouldFetch(state) {
    //  return true
    //}
    
    // this function checks in our local cache first
    // if the value is present it'll use that instead (optional).
    local(state) {
      return state.videogames.size > 0 ? state.videogames : null;
    },
    
    // remotely fetch something (required)
    remote (state) {
      return $.get('/video_games', function (results) { return results } );
    },

    // here we setup some actions to handle our response
    //loading: videoGameActions.loadingResults, // (optional)
    success: videoGameActions.updateVideoGames, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  },
  
  fetchVideoGameDetail: {
    //shouldFetch(state) {
    //  return true
    //}

    local(state, id) {
      return state.videogame.get("id") == id ? state.videogame : null;
    },
    
    remote (state, id) {
      return $.get('/video_games/' + id, function (result) { return result } );
    },

    loading: videoGameActions.videoGameLoading, // (optional)
    success: videoGameActions.updateVideoGameDetail, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  },
  
  updateVideoGameNameServer: {
    //shouldFetch(state) {
    //  return true
    //}

    //local(state) {
    //  return state.results[state.value] ? state.results : null;
    //},
    
    remote (state, id, newname) {
      return $.ajax( { url: '/video_games/' + id, type: 'put', data: {id: id, name: newname} } );
    },

    //loading: videoGameActions.loadingResults, // (optional)
    success: videoGameActions.updateVideoGameNameServer, // (required)
    error: videoGameActions.videoGameFailed, // (required)
  }
  
};

In this file, we do all the operations that are linked to the server, such as fetching data, or updating database data. See the alt Data Sources documentation for more information.

A brief description of each function:

  • fetchVideoGames: this function gets the list of video games from the server. It first checks the local cache to see if video games have already been retrieved, and if not, it sends an ajax request to the server to get the list. When the results arrive, it updates the video games list by calling « updateVideoGames » action.
  • fetchVideoGameDetail: this function gets the detail of video games from the server. It first checks the local cache to see if the details of this video game have already been retrieved, and if not, it sends an ajax request to the server to get them. When the result arrives , it updates the video games detail page by calling « updateVideoGameDetail » action. When the data is being fetch, the videoGameLoading action is called, which displays a « loading div ».
  • updateVideoGameNameServer: this function updates the name of the video game in the database. It sends an ajax request to the server.

stores directory

Create /app/assets/javascripts/stores directory, and create a VideoGameStore.es6 file in it, with the following content:

/app/assets/javascripts/sources/VideoGameStore.es6

class VideoGameStore {

  constructor () {
    this.videogames = new Immutable.List();
    this.videogame = new Immutable.Map();
    this.videogameSelected = new Immutable.Map();
    this.errorMessage = null;
    
    this.bindActions(videoGameActions);
    this.registerAsync(VideoGameSource);
  }
  
  onUpdateVideoGames (videogames) {
    this.videogames = this.videogames.mergeDeep(videogames);
  }
  
  onUpdateVideoGameDetail (videogame) {
    this.videogame = this.videogame.merge(videogame);
  }
  
  onVideoGameLoading () {
    this.videogame = new Immutable.Map();
  }
  
  onUpdateVideoGameName (name) {    
    this.videogame = this.videogame.set("name", name);
  }
  
  onSelectVideoGame (videogame) {
    this.videogameSelected = this.videogameSelected.merge(videogame);
  } 
  
  onVideoGameFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }
  
  onUpdateVideoGameNameServer(videogame) {    
    //we want to re-initialize state so that we do not display old values when reloading components
    this.videogames = new Immutable.List();
    this.videogameSelected = new Immutable.Map();
    this.videogame = new Immutable.Map( { id: videogame.id, name: videogame.name } );
  }

}

var videoGameStore = alt.createStore(VideoGameStore, 'VideoGameStore');

Here, you can see that we initialize the store state with Immutable objects. Using immutable objects can greatly improve performance, facilitating the comparison between old and new states/props (in our case thanks to the PureRenderMixin addon).

The bindActions function in the constructor enables to link the actions to the store, and more precisely to the functions inside the store. The link work only if your store functions start with « on » + « the name of the action » (see createStore Alt documentation for more information).

The registerAsync function in the constructor enables to link the store to the data source. Thanks to this, you will be able to call data source functions directly from the store (ex: videoGameStore.fetchVideoGames(), videoGameStore.fetchVideoGameDetail(), …).

And now a brief description of each function:

  • onUpdateVideoGames: updates the video games list (if it has changed) and therefore refreshes « video games list » or « best video games list ». We use the « mergeDeep » immutable function because the video games list contains video game objects.
  • onUpdateVideoGameDetail: updates the video game detail (if it has changed) and therefore refreshes the video game detail page.
  • onVideoGameLoading: re-initializes the videogame object, which enables to display a « loading div » on the video game detail page.
  • onUpdateVideoGameName: updates the name of the video game and therefore displays the changes instantly on the video game detail page.
  • onSelectVideoGame: replaces videogameSelected with the new selected video game (if it has changed), and therefore refreshes the selected video game.
  • onVideoGameFailed: initializes errorMessage if there is an fetching data error, and displays the error message on the page.
  • onUpdateVideoGameNameServer: updates the videogame object with the updated video game, and re-initializes video games list and video game selected to display the new values.

containers directory

Create /app/assets/javascripts/containers directory. This directory is going to contain all the containers that are used in this application.

From AltContainer documentation:

The basic idea is that you have a container that wraps your component, the duty of this container component is to handle all the data fetching and communication with the stores, it then renders the corresponding children. The sub-components just render markup and are data agnostic thus making them highly reusable.

Create a videogame-best-container.jsx.erb file inside, which has for purpose to display the « best video games list » page, with the following content:

/app/assets/javascripts/containers/videogame-best-container.jsx.erb

class VideoGameBestContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGames();
  }

  render () { 
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameBestComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameBestContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this) //allows the use of 'this' in onChange
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGames();
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <VideoGameBestComponent videogames={this.state.videogames} errorMessage={this.state.errorMessage} />
    )
  }
}
*/

Create a videogame-list-container.jsx.erb file inside, which has for purpose to display the « video game list » page, with the following content:

/app/assets/javascripts/containers/videogame-list-container.jsx.erb

class VideoGameListContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGames();
    videoGameActions.selectVideoGame(null);
  }

  render () {
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameListComponent />
        <VideoGameSelectedComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameListContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this)
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGames();
    videoGameActions.selectVideoGame(null);
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <div>
        <VideoGameListComponent videogames={this.state.videogames} videogameSelected={this.state.videogameSelected} />
        <VideoGameSelectedComponent videogameSelected={this.state.videogameSelected} />
      </div>
    )
  }
}
*/

 

Create a videogame-detail-container.jsx.erb file inside, which has for purpose to display the « video game details » page with the following content:

/app/assets/javascripts/containers/videogame-detail-container.jsx.erb

class VideoGameDetailContainer extends React.Component {
  componentDidMount () {
    videoGameStore.fetchVideoGameDetail(this.props.params.id);
  }

  render () {
    return (
      <AltContainer store={videoGameStore}>
        <VideoGameDetailComponent />
      </AltContainer>
    )
  }
}


/*
//Without AltContainer
class VideoGameDetailContainer extends React.Component {
  constructor () {
    super();
    this.state = videoGameStore.getState();
    this.onChange = this.onChange.bind(this);
  }
  
  onChange (state) {
    this.setState(state);
  }
  
  componentDidMount () {
    videoGameStore.listen(this.onChange);
    videoGameStore.fetchVideoGameDetail(this.props.params.id);
  }
  
  componentWillUnmount () {
    videoGameStore.unlisten(this.onChange);
  }
  
  render () {
    return (
      <VideoGameDetailComponent videogame={this.state.videogame} />
    )
  }
}
*/

I like using the AltContainer library (AltContainer.js file) because it brings a lot of automatic mechanisms that keep the code clear, but if you do not want to use it, just take the commented part at the end of each file. In this case, do not forget to:

  • implement the « onChange » method,
  • bind it to « this » in the constructor,
  • « listen » to the store in the « componentDidMount » method
  • and « unlisten » the store in the « componentWillUnmount » method.

components directory

Create /app/assets/javascripts/components directory. This directory is going to contain all the components of the application.

Create an app-component.jsx.erb file inside, which has for purpose to display the navigation links and the component rendered by routes.jsx, with the following content:

/app/assets/javascripts/components/app-component.jsx.erb

//Stateless component
const AppComponent = (props) =>
  <div>
    <h1>Video Games</h1>
    <nav>
      <Link to='/private/video-game-best'>Best Video Games</Link>
      <Link to='/private/video-game-list'>Video Games List</Link>       
    </nav>
    {props.children}
  </div>;

Have a look at « props.children » which returns what the routes (routes.jsx) tells him to.

Create a videogame-best-component.jsx.erb file in the directory, which has for purpose to display the best video games, with the following content:

/app/assets/javascripts/components/videogame-best-component.jsx.erb

class VideoGameBestComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleDetailClick (id) {
    browserHistory.push('/private/video-game-detail/' + id);
  }

  render () {
    if (this.props.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (!this.props.videogames.size) {
      return (
        <div>Loading Best Video Games...</div>
      );
    }
    else {
      return (
        <div>
          <h3>Best Video Games</h3>
          <div className='grid grid-pad'>
            { this.props.videogames.slice(0,4).map(
              videogame => {
                return (
                  <div key={videogame.get("id")} className='col-1-4' >
                    <div className='module videogame' onClick={this.handleDetailClick.bind(this,videogame.get("id"))}><h4>{videogame.get("name")}</h4></div>
                  </div>    
                )
              }
            )}
          </div>
          <div className='image-div'>
            <img src="<%= asset_path('my-image.png') %>" alt="logo" />
          </div>
        </div>      
      )
    }
  }
}

Create a videogame-list-component.jsx.erb file in the directory, which has for purpose to display the full list of video games, with the following content:

/app/assets/javascripts/components/videogame-list-component.jsx.erb

class VideoGameListComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleClick (videogame) {
    videoGameActions.selectVideoGame(videogame);
  }
  
  render () {
    return (
      <div>
        <h2>My Video Games</h2>
        <ul className='videogames'>      
          { this.props.videogames.map(
            videogame => {
              var liClass = '';
              if(this.props.videogameSelected.size == 0 && (this.props.videogameSelected == videogame)) { liClass = 'selected' };
              return (
                <li key={videogame.get("id")} className={liClass} onClick={this.handleClick.bind(this,videogame)}>
                  <span className='badge'>{videogame.get("id")}</span> {videogame.get("name")}
                </li>
              )
            }
          )}
        </ul>
      </div>
    )
  }
}

Create a videogame-selected-component.jsx.erb file in the directory, which has for purpose to display the video game selected in the video game list component, with the following content:

/app/assets/javascripts/components/videogame-selected-component.jsx.erb

class VideoGameSelectedComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  handleDetailClick (id) {
    browserHistory.push('/private/video-game-detail/' + id);
  }
  
  render () {
    return (
      <div>
        { ( () => {
            if(this.props.videogameSelected.size > 0) {
              return (
                <div>
                  <h2>{this.props.videogameSelected.get("name").toUpperCase()} selected</h2>
                  <button onClick={this.handleDetailClick.bind(this,this.props.videogameSelected.get("id"))}>View Details</button>
                </div>
              )
            }
          } )()
        }
      </div>
    )
  }
}

Finally, create a videogame-detail-component.jsx.erb file in the directory, which has for purpose to display the details of the video game (selected by a click in the « best video games » list or the « full video games » list), with the following content:

/app/assets/javascripts/components/videogame-detail-component.jsx.erb

class VideoGameDetailComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  
  handleChange (e) {
    name = e.target.value;
    videoGameActions.updateVideoGameName(name);
  }
  
  goBackClick (id) {
    window.history.back();
  }
  
  updateVideoGameClick () {
    videoGameStore.updateVideoGameNameServer(this.props.videogame.get("id"),this.props.videogame.get("name"));
    alert("updated!");
  }
  
  render () {
    if (this.props.errorMessage) {
      return (
        <div>Something is wrong</div>
      );
    }
    else if (this.props.videogame.size == 0) {
      return (
        <div>Loading Video Game...</div>
      );
    }
    else {
      return (
        <div>
          <h2>{this.props.videogame.get("name")} details</h2>
          <div>
            <label>id: </label>{this.props.videogame.get("id")}
          </div>
          <div>
            <label>name: </label>
            <input type="text" value={this.props.videogame.get("name")} placeholder={this.props.videogame.get("name")} onChange={this.handleChange} />
          </div>
          <button onClick={this.goBackClick}>Back</button>
          <button onClick={this.updateVideoGameClick.bind(this)}>Update</button>
        </div>
      )
    }
  }
}

I will not detail each file but notice these few things:

  • this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); is written in each constructor. It enables to do a « shallow compare » between old props and new props, and it renders the component only if the props have changed. It is one of the major clue for performances. To use it, your component has to be « pure », that is to say it renders the same result given the same props and state (see React PureRenderMixin Documentation).
  • actions are made either by calling « videoGameActions » or « videoGameStore » in the case of data sources actions (such as updateVideoGameNameServer). The call of « videoGameStore » is possible thanks to registerAsync in VideoGameStore.
  • in loops, you have to use the « key » element, it is pure React (do not worry, if you forget you will have a nice error in your console)

2.2 – Stylesheets configuration

The explanation of CSS is out of this scope, so please, just copy the following files in the app/assets/stylesheets directory.

Create style.css

/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: #000; 
  font-family: Cambria, Georgia; 
}

button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
}

button:hover {
  background-color: #cfd8dc;
  cursor: pointer;
}

button:disabled {
  background-color: #eee;
  color: #aaa; 
  cursor: pointer;
}

/* everywhere else */
* { 
  font-family: Arial, Helvetica, sans-serif; 
}

Create app.component.css

/app/assets/stylesheets/app.component.css

h1 {
  font-size: 2em;
  color: #000;
  margin-bottom: 0;
}

h2 {
  font-size: 1.5em;
  margin-top: 0;
  padding-top: 0;
}

nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin: 15px 10px 30px 0;
  display: inline-block;
  background-color: #227dd3;
  border-radius: 4px;
}

nav a:visited, a:link {
  color: #FFF;
}

nav a:hover {
  color: #E1E0E0;
  background-color: #208ef6;
}

nav a.router-link-active {
  color: #039be5;
}

Create video-game-best.component.css

/app/assets/stylesheets/video-game-best.component.css

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

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

.videogames li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #ea6422;
  margin: .5em;
  padding: .25em 0;
  height: 2em;
  border-radius: 15px;
}

.videogames li.selected:hover {
  background-color: #ffc33e !important;
  color: white;
}

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

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

.videogames .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #ffc33e;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 2.5em;
  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: 2em;
}

[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: #ffc33e;
    max-height: 120px;
    min-width: 120px;
    background-color: #ea6422;
    border-radius: 50px;
}

h4 {
  position: relative;
}

.module:hover {
  background-color: #ffc33e;
  cursor: pointer;
  color: #ea6422;
}

.image-div {
  text-align: center;
}

.image-div img {
  width: 6em;
}

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

Create video-game-detail.component.css

/app/assets/stylesheets/video-game-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 {
  background-color: #ff992b;
  color: #FFF; 
  margin: 20px 5px 0 0;
  font-family: Arial;
  border: none;
  padding: 5px 10px;
}

button:hover {
  background-color: #ed8a1f;
}

button:disabled {
  color: #ccc; 
}

3 – Start the server and see the results

rails s

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

Do not forget to create a user on: http://your-server/users/sign_up

We are done! If you have any suggestion of how to improve this post, please, let me know.

Publicités

2 réflexions sur “Rails and React – Full tutorial

    • Hello Justin, thank you for your advise. I have seen a bit of your work and your other tutorials, and they look great! So I do appreciate your feedback. However, I prefer react-rails gem to react_on_rails one (I actually tried both). Indeed, with react-rails I can add just the libraries I need (for example I prefer Alt to Redux, which seems to be embedded in react_on_rails gem).

      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