React and Immutable.js

A quick view on performances

Our application is already quite fast as React handles the virtual DOMs comparison and is very efficient for that (and also because the application is really small 😉 ).

But if you take a deeper look you will notice something strange: everytime you go on a page, the associated component is rendered 3 times. To check that, add a console.log below every render function in your app-component, dashboard-component, heroes-component and hero-detail-component:

class DashboardComponent extends React.Component {
  ...
  render () {
    console.log("render DashboardComponent");
    ...
  }
}
class HeroesComponent extends React.Component {
  ...
  render () {
    console.log("render HeroesComponent");
    ...
  }
}

And so on…

Now, open a Development console in your browser and wander around your different pages: you will see in your console that a same component is often rendered 3 times right away.

Why that? The first render is due to the fact that React calls the previous known version of the component and renders it. Then your action (like fetching heroes or fetching hero details) is called in the componentDidMount function, and if you remember we  do a dispatch() to the store  before fetching the data (in order to display a « loading » div): this renders the component again. Finally, we get the result of our fetching and we dispatch once more to the store, which renders the component a last time.

shouldComponentUpdate and PureRenderMixin

React explains how to avoid re-rendering all the time here: Advanced Performance. To summarize, it uses the shouldComponentUpdate function, that enables to « short circuit » the process. It means that you can use this function in your component to decide whether or not you want to render it.

Most of the time, you will want to render the component only if its state or its props have changed. Therefore, React offers a function that will do the comparison between the previous state and the new state, and between the previous props and the new props. This function is called: PureRenderMixin (documentation is here: PureRenderMixin), and it uses the shouldComponentUpdate function presented above. To use it, your component has to be « pure », which means from the React documentation that: « it renders the same result given the same props and state ». This notion is still vague for me 😉

Immutable.JS

Even though several people had talked to me about Immutable, it took me a little while to understand what was its purpose in a React application. It was only when dealing with shouldComponentUpdate and PureRenderMixin functions that I got the main idea: Immutable enables to compare deeply and easily two Javascript Objects. Indeed, when using the « = » comparison it will work with simple values such as integers or strings, but not with array, hash or complex data structures, because in these last cases « = » will compare the references of the objects and not the values inside them. When dealing with state and props, we want to compare the values and not the references, therefore Immutable takes all its meaning here!

Do not hesitate to have a look at the official documentation: Immutable.js.

Rails and React and Alt project updated with PureRenderMixin and Immutable

Let’s see how we can improve the performances of our Rails and React and Alt “Tour of Heroes” tutorial with what we have seen above.

1 – Rails configuration

1.1 – Get Immutable and put it in your vendor directory

First, we are going to download the Immutable javascript file and put it in our vendor directory (where we already have alt.js and ReactRouter.min.js). 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.2 – Activate PureRenderMixin add-on

By default, the react-rails gem does not include React add-ons, but it allows you to do it by simply adding this line in your application.rb file: config.react.addons = true. Do not forget to restart your rails server after this operation.

In 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
    
    #reactjs
    config.react.addons = true
  end
end

1.3 – Update heroes_controller.rb

We have a small modification to do here. So far, the update method of heroes_controller was not returning anything. We did not mind as we were rendering all components all the time. But now, as we want to render only when required, we will need to get a result from the update method, so that we will know that we have to refresh (ie render) the components.

Let’s add this code to the end of our heroes_controller.rb:

    hero = { id: params[:id], name: params[:name] }
    respond_to do |format|
      format.json { render json: hero }
    end

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)
    
    hero = { id: params[:id], name: params[:name] }
    respond_to do |format|
      format.json { render json: hero }
    end
  end

end

2 – Javascript configuration

2.1 – Update application.js

We have to include the immutable.min.js library so we add it in our applications.js file.

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs

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

2.2 – Create addons_init.js

The only React add-on we will use as for now is PureRenderMixin. You can initialize it by creating an addons_init.js file in your javascripts assets directory with the following line:

In app/assets/javascripts/addons_init.js:

var PureRenderMixin = React.addons.PureRenderMixin;

2.3 – Update components.js

We must now include this new addons_init.js into our components.js file.

In app/assets/javascripts/components.js:

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

2.4 – Update HeroActions.es6

We are now going to update our HeroActions file. First, I suggest we remove the two dispatch() functions in fetchHeroes () and fetchHeroDetail (id) because we do not want to display the loading div anymore, we rather want to reduce the number of renders (we only render if state or props have changed). Second, we modify a little bit updateHeroNameServer (id,newname) to dispatch the result of our modified heroes_controller update method.

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) => {
      $.get('/heroes', function (results) { return results } )
        .done((heroes) => { this.updateHeroes(heroes) } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  fetchHeroDetail (id) {
    return (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((hero) => { dispatch(hero); } )
        .fail((errorMessage) => { this.heroesFailed(errorMessage) } );
    }
  }
  
  heroesFailed (errorMessage) {
    return errorMessage;
  }
  
}

var heroActions = alt.createActions(HeroActions);

2.5 – Update HeroStore.es6 (immutable)

In our HeroStore, we remove the onFetchHeroes () and the onFetchHeroDetail () functions as we do not need them anymore. They were needed to render the loading div when calling dispatch() but we removed them both in our HeroAction.

In the constructor we introduce Immutable by initializing our variables with Immutable objects instead of classical javascript objects.

Then we create an onUpdateHeroNameServer (hero) function that will be called when doing the dispatch(hero) in HeroAction. In this function we reinitialize the Immutable objects so that our components will know that they have to render.

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

class HeroStore {

  constructor () {
    this.heroes = new Immutable.List();
    this.hero = new Immutable.Map();
    this.heroSelected = new Immutable.Map();
    this.errorMessage = null;
    
    this.bindActions(heroActions);
  }
  
  onUpdateHeroes (heroes) {
    this.heroes = this.heroes.mergeDeep(heroes);
  }
  
  onUpdateHeroDetail (hero) {
    this.hero = this.hero.merge(hero);
  }
  
  onUpdateHeroName (name) {    
    this.hero = this.hero.set("name", name);
  }
  
  onSelectHero (hero) {
    this.heroSelected = this.heroSelected.merge(hero);
  } 
  
  onHeroesFailed (errorMessage) {
    this.errorMessage = errorMessage;
  }
  
  onUpdateHeroNameServer(hero) {    
    //we want to re-initialize state so that we do not display old values when reloading components
    this.heroes = new Immutable.List();
    this.heroSelected = new Immutable.Map();
    this.hero = new Immutable.Map( { id: hero.id, name: hero.name } );
  }

}

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

2.6 – Update your components (PureRenderMixin and immutable)

All we are going to do in our components is to add the PureRenderMixin function (which will compare previous state with new state and previous props with new props) and transform all calls to state objects by adding a get as we are now dealing with Immutable objects (example: replace hero.id with hero.get(« id »)).

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

class DashboardComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    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.size) {
      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.get("id")} className='col-1-4' >
                    <div className='module hero' onClick={this.handleDetailClick.bind(this,hero.get("id"))}><h4>{hero.get("name")}</h4></div>
                  </div>    
                )
              }
            )}
          </div>
        </div>      
      )
    }
  }
}

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

class HeroesComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    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.size == 0 && (this.state.heroSelected == hero)) { liClass = 'selected' };
            return (
              <li key={hero.get("id")} className={liClass} onClick={this.handleClick.bind(this,hero)}>
                <span className='badge'>{hero.get("id")}</span> {hero.get("name")}
              </li>
            )
          }
        )}
      </ul>
      { ( () => {
          if(this.state.heroSelected.size > 0) {
            return (
              <div>
                <h2>{this.state.heroSelected.get("name").toUpperCase()} is my hero</h2>
                <button onClick={this.handleDetailClick.bind(this,this.state.heroSelected.get("id"))}>View Details</button>
              </div>
            )
          }
        } )()
      }
    </div>
    )
  }
}

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

class HeroDetailComponent extends React.Component {
  constructor () {
    super();
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
    this.state = heroStore.getState();
    this.onChange = this.onChange.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.get("id"),this.state.hero.get("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.get("name")} details</h2>
          <div>
            <label>id: </label>{this.state.hero.get("id")}
          </div>
          <div>
            <label>name: </label>
            <input type="text" value={this.state.hero.get("name")} placeholder={this.state.hero.get("name")} onChange={this.handleChange} />
          </div>
          <button onClick={this.goBackClick}>Back</button>
          <button onClick={this.updateHeroClick.bind(this)}>Update</button>
        </div>
      )
    }
  }
}

(optional) Transform app-component to a Stateless function

Our app-component is stateless so we can transform it as explained at the end of this page Reusable Components in React documentation.

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

//Stateless component
const AppComponent = (props) =>
  <div>
    <h1>Tour of Heroes</h1>
    <nav>
       <Link to='/heroes-list'>Heroes</Link>
       <Link to='/dashboard'>Dashboard</Link>
    </nav>
    {props.children}
  </div>;

Conclusion

Now, if you add the console.log again right below the render function of your components, you will see that render is not called as often as before. It is only called when the state changes. It is obviously fast when switching from heroes list to dashboard, as the heroes list does not change.

Publicités

5 réflexions sur “React and Immutable.js

  1. Thanks.
    By « fetched by Google », you mean that you want Google to be able to crawl your website?
    As far as I know, Google does crawl javascript generated pages (it seems it has already been the case for more than one year: http://searchengineland.com/tested-googlebot-crawls-javascript-heres-learned-220157).
    However, it is still a good question as other search engines may not do so.

    I will soon write a full example with a public part, which will be server side rendered to facilitate SEO, and a private part fully client rendered (because in this case we do not care of SEO).

    But to quickly give you an answer you have to do the following steps:
    – add in your application.rb file:
    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
    }
    – add « //= require prerender » to your components.js file (you have to include your prerendered files)
    – create a prerender.js file with « //= require_tree ./prerender »
    – create a « prerender » directory in which you will put all your server prerendered files (for example a home-component.jsx.erb file with a HomeComponent inside)
    – and finally, in your controller, call the file like this:

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

    My explanation is really light, so let me know if you get stucked somewhere…

    J'aime

    • yes, you understood me correct.
      Sorry for making you checking react-rails documentation.

      What I see here is that we are moving rendering on server (redux) in order to make it crawled by search engines. Which is probably fine as we use react. Two questions came to my mind.

      Q1: It also looks like I have to configure rest of routes like /dashboard, /heroes in order to make my site SEO friendly. Is it correct? Maybe react-rails does it out of the box? Just curious, the tutorial is exactly what I was looking for the past month 🙂

      Q2: It seems to be over-engineered by having too many things configured and installed. Am I the only one who get this feeling? Rails does not seem to play well with npm and node js stuff.
      This is not a critic, do not get me wrong, I am always looking for an easier way.

      J'aime

  2. Yes, sorry, I was a little too quick in my explanation.
    You are right, we render the view on server side so that we generate an HTML page that will be more easily read by Search Engine (you can see the difference by « displaying the source code of the page »).
    I prefer, when possible, rendering on client side so that we keep the server from doing rendering task that can be performed by clients, but for SEO I think that it is a good behavior to render on server side the pages that should be referenced.

    Q1: yes, exactly, you have to configure in your rails « routes.rb » file all the routes that will be rendered on server side. I do not know any out of the box way to do it (but maybe there is?).

    Q2: I encountered a few problems when playing with npm and node modules (the first time it did not work, and the second time it worked but downloaded over 500Mb of node modules!). Moreover, I like to understand the purpose of each file of my project (or the global meaning when using gems for example), so I was not really fond of having something that was downloading a lot of things I had no clue about…
    So I decided to work without npm, and it got along pretty well. However, as you can see, we need to install a few gems and js librairies so it can still look a little over-engineered, but I prefer to do that and understand step by step what I am doing.

    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