Codementor Events

Understanding React JS Rendering

Published Feb 08, 2015Last updated Apr 12, 2017
Understanding React JS Rendering

So, there is a new architecture in town, FLUX, and it is starting to get some solid ground. There are several implementations of it, amongst those Facebook Flux, Yahoo Dispatchrı, Fluxxor, McFly and jFlux, which I have personally been working on. One of the core conceps in FLUX is stores. A store will emit a change event whenever any of the state held within the store changes. Any components listening to changes to stores will grab state from the respective store and render. In this article we are going to take a look at the rendering of React JS when it operates in a traditional FLUX environment.

The Change Event

When reading documentation on the FLUX architecture you will surely meet the "change event". Basically whenever a store has done a change to some state it will emit this one type of event, change. By having only one type of event your application will of course be more managable in terms of keeping all your components in sync with the stores, but it also has a cost. Lets look at an example with a flux-react store:

var store = flux.createStore({
  todos: [],
  addTodo: function (todo) {
    this.todos.push(todo);
    this.emit('change');
  },
  removeTodo: function (index) {
    this.todos.splice(index, 1);
    this.emit('change');
  },
  exports: {
    getTodos: function () {
      return this.todos;
    }
  }
});

Any components listening to a change event on this store would use the getTodos method, when todos are added and removed, to get the updated state. That makes sense and it is absolutely nothing wrong with that as it is highly likely that any component interested in adding todos would probably also be interested in their removals. But let us add another state:

var store = flux.createStore({
  todos: [],
  isSaving: false,
  addTodo: function (todo) {
    this.todos.push(todo);
    this.isSaving = true;
    this.emit('change');
    doSomethingAsync().then(function () {
      this.isSaving = false;
      this.emit('change');
    }.bind(this));
  },
  removeTodo: function (index) {
    this.todos.splice(index, 1);
    this.isSaving = true;
    this.emit('change');
    doSomethingAsync().then(function () {
      this.isSaving = false;
      this.emit('change');
    }.bind(this));
  },
  exports: {
    getTodos: function () {
      return this.todos;
    },
    isSaving: function () {
      return this.isSaving;
    }
  }
});

Now we are first triggering a change to notify our components that the store is in isSaving state and that we have a new todo. Later we notify our components again about the store not saving anymore. With this simple example we are starting to see where the cost is increasing. You might argue that there should not be async operations inside stores, but bear with me on that. In this case we are focusing on the use of a general 'change' event both for notifying about our isSaving state and the update to our todos state. Let's visualize what I mean here. Imagine this HTML being components:

<div>
  <AddTodoComponent/>
  <TodosListComponent/>
</div>

We want the input inside <AddTodoComponent/> to disable itself while the store is in isSaving state. It does that by listening to a change event in the store. In addition to this we also want our <TodosListComponent/> to update itself when there are changes to the todos array and we of course listen to the same change event to accomplish that. So what happens is the following:

  1. We grab both isSaving and todos when components are created
  2. We add a new todo causing a "change" event to occur
  3. The <AddTodoComponent/> grabs the new isSaving state, disabling itself, and the <TodosListComponent/> grabs the mutated todos state to show the new todo in the list
  4. When the async operation is done we trigger a new change event causing again our two components to grab the same states, though the <TodosListComponent/> did not really have to, since there were no new mutations on the todos array

Reacting unnecessarily to state changes is not the only cost though, lets look a little deeper.

React JS Cascading Renders

One important detail about React JS that is often overlooked is how setState on a component affects the nested components. When you use setState it is not only the current component that will do a render, but also all nested components. That means if a change event is being listened to on your application root component and a change event is triggered from the store, all your components will do a render and a diff to produce any needed DOM operations. Let's visualize this:

[Cascading render]

               /---\
               | X | - Root component renders
               |---|
                 |
            /----|---\
            |        |
          /---\    /---\
          | X |    | X | - Nested components also renders
          |---|    |---|              

But if a nested component does a setState it will not affect parent components.

[Cascading render]

               |---|
               |   | - Root component does not render
               |---|
                 |
            /----|---\
            |        |
          /---\    /---\
          |   |    | X | - Nested component renders
          |---|    |---|          

This actually means that you could get away with only listening to changes in stores on your root component, triggering a setState and then just grab state directly from the stores in the nested components. We can create an example of this with our TodoApp:

var TodoApp = React.createClass({
  componentWillMount: function () {
    AppStore.on('change', this.update);
  },
  componentWillUnMount: function () {
    AppStore.off('change', this.update);
  },
  update: function () {
    this.setState({}); // Just trigger a render
  },
  render: function () {
    return (
      <div>
        <AddTodoComponent/>
        <TodosListComponent/>
      </div>
    );
  }
});

This component is just listening to a general change event and triggers a render. As stated above this will cascade down to the nested components, so if we f.ex. in our <AddTodoComponent/> do this:

var AddTodoComponent = React.createClass({
  render: function () {
    return (
      <form>
        <input type="text" disabled={AppStore.isSaving()}/>
      </form>
    );
  }
});

That is actually all we need to handle the disabled state. There is not need to listen for changes because our root component does that for us.

This gives you predictability in the rendering of your application, but there is of course a balance. Lets say you are already listening to most change events at the root component of you application, that would be a situation where you might consider removing additional nested listeners as they will only cause unnecessary renders. But if you have a very flat component structure that listens to many different stores that would of course not be such a good idea. Lets dive a bit deeper.

Repeated Rendering

A general change event triggering a setState on a component will not only cause cascaded rendering but will also cause repeated rendering in components. Let me explain:

[Repeated rendering]

               /---\
               |   | - Root component listens to change
               |---|
                 |
            /----|---\
            |        |
          /---\    /---\
          |   |    |   | - Nested components listens to change
          |---|    |---|           

When a change event now occurs the root component will first trigger a render:

[Repeated rendering]

            /---\
            | X | - Root component reacts to change event and renders
            |---|
              |
         /----|---\
         |        |
       /---\    /---\
       | X |    | X | - Nested components render
       |---|    |---|           

And after that, the nested components will actually render themselves again:

[Repeated rendering]

       /---\
       |   | -
       |---|
         |
    /----|---\
    |        |
  /---\    /---\
  | X |    | X | - Nested components react to change event and renders
  |---|    |---|         

Now if there were deeper nested components this would cause the same effect, but with even more repeated rendering due to each nested level causes an extra render.

If this is new to you your reaction might be in the genre of "yikes", but React JS is extremely fast and in simple applications this is not an issue at all. But if you are like me, you want to know how your code runs, and now you do. So lets look at some solutions to optimize this behavior.

Optimizing

First I would like to take a minute to look at the shouldComponentUpdate method. This is used to control the behavior of cascading rendering. Lets look at our visualization again:

[Cascading render]

               /---\
               | X | - Root component renders
               |---|
                 |
            /----|---\
            |        |
          /---\    /---\
          | X |    | X | - Nested components also renders
          |---|    |---|           

If our nested components had the shouldComponentUpdate method and it returned false:

var NestedComponent = React.createClass({
  shouldComponentUpdate: function () {
    return false;
  },
  render: function () {
    return (
      <div></div>
    );
  }
});

This would be the result:

[Render cascading]

               /---\
               | X | - Root component renders
               |---|
                 |
            /----|---\
            |        |
          /---\    /---\
          |   |    |   | - Nested components do not render
          |---|    |---|  

But it is a pain to add this to all your components. What you could do instead is add a mixin from React addons, called PureRenderMixin. This will have the same effect.

var NestedComponent = React.createClass({
  mixins: [React.addons.PureRenderMixin],
  render: function () {
    return (
      <div></div>
    );
  }
});

This certainly improves the rendering performance, but still every single component listening to a change will try to do a new render, even if there was a state change the component did not care about. In addition to this you have to make sure that any object or arrays set on your props or state will change their reference when changed. This is because PureRenderMixin only does a shallow check.

Preventing Unnecessary Renders

One example of controlling renders is to use more than the single "change" event. flux-react uses EventEmitter2 to allow for namespaced events. Let me just show you an example:

var TodosListComponent = React.createClass({
  mixins: [flux.RenderMixin],
  componentWillMount: function () {
    AppStore.on('todos.*', this.changeState);
  },
  componentWillUnmount: function () {
    AppStore.off('todos.*', this.changeState);
  },
  changeState: function () {
    this.setState({
      todos: AppStore.getTodos()
    });
  },
  render: function () {
    return (
      <ul>
        {this.state.todos.map(function (todo) {
          return <li>{todo.title}</li>
        })}
      </ul>
    );
  }
});

The flux-react RenderMixin does the same work as PureRenderMixin, but now you do not have to include the React addons to get the benefits.

As you can see I used an asterix wildcard when listening for events. This actually means that the store emits the following event on adding a todo, this.emit('todos.add'), and, this.emit('todos.remove'), when removing a todo. Our TodosListComponent will act upon both events. Some other component might only be interested in adding a todo and thus only try to render again when that specific event happens. This will of course add complexity to your application, but you get more control of rendering and you can also react to transitions in state, which is a challenge with traditional flux. Let me explain.

Lets say you have an iframe controlled by a component that should refresh every time a todo is added... for whatever reason. How would you handle that with a "change" event? You would probably have to keep a reference to the number of todos inside the component and on all change events you would have to verify the length kept in the component with the length from the store. That is not a good way to do it. When emitting a "todos.add" event though you know that a todo was actually added and you can safely refresh the iframe on every occurrence of that event.

Listening to specific state transitions is something that is required in modern single page applications. Route changes, animations and other types of transitions is very difficult to handle with a single "change" event.

State Tree

It is also possible to use a state tree, like Baobab. Personally I think this is the next evolution of the flux architecture. It solves so many challenges and keeps an amazingly simple API. Lets look at an example first:

var Baobab = require('baobab');
var stateTree = new Baobab({
  todos: []
}, {

  // We add the PureRenderMixin to all our components using
  // a tree or cursor mixin
  mixins: [React.addons.PureRenderMixin],

  // When the tree updates it makes sure all objects and arrays
  // change their reference so PureRenderMixin can do its shallow
  // check
  shiftReferences: true
});

var todosCursor = stateTree.select('todos');
var TodosListComponent = React.createClass({

  // We add a mixin from our cursor which listens for
  // updates and triggers a render. The React.PureRenderMixin
  // is also included
  mixins: [todosCursor.mixin],

  // The data on the cursor is available on your state, via
  // the cursor property
  render: function () {
    return (
      <ul>
        {this.state.cursor.map(function (todo) {
          return <li>{todo.title}</li>
        })}
      </ul>
    );
  }
});

Baobab allows you to create a state tree that you can point to with cursors. You can listen to changes on these cursors, like in the example above we listen to changes on "todos". This gives a very simple API with very little boilerplate that handles optimization for you. Baobab has a few other tricks up its sleeve that accommodates flux architecture so I encourage you to check it out.

Summary

This article has no intention of pointing you into a "best practice". Its intent is to give you some insight into how React JS rendering works and how the flux implementation you choose affects React JS rendering. To round this up I just want to say that I hope this article contributed to a bit more understanding of how React JS works. Flux is still a very new concept and we are probably going to see even more implementations for some time, but React JS is definitely here to stay.

Discover and read more posts from Christian Alfoni
get started
post commentsBe the first to share your opinion
ii
7 years ago

ii

Show more replies