React, Redux and Immutable.js: Ingredients for Efficient Web Applications

fcqxqyslzx 8年前

来自: http://www.toptal.com/react/react-redux-and-immutablejs

React, Redux and Immutable.js are currently among the most popular JavaScript libraries and are rapidly becoming developers’ first choice when it comes to front-end development . In the few React/Redux projects that I have worked on, I realised that a lot of developers getting started with React do not fully understand React and how to write efficient code to utilise its full potential.

In this article we will build a simple app using React , Redux and Immutable.js , and identify some of the most common misuses of React and ways to avoid them.

Data Reference Problem

React is all about performance. It was built from the ground up to be extremely performant, only re-rendering minimal parts of DOM to satisfy new data changes. Any React app should mostly consist of small simple (or stateless function) components. They are simple to reason about and most of them can have shouldComponentUpdate function returning false .

shouldComponentUpdate(nextProps, nextState) {     return false;  }

Performance wise, the most important component lifecycle function is shouldComponentUpdate and if possible it should always return false . This ensures that this component will never re-render (except the initial render) effectively making the React app feel extremely fast.

When that is not the case, our goal is to make a cheap equality check of old props/state vs new props/state and skip re-rendering if the data is unchanged.

Let’s take a step back for a second and review how JavaScript performs equality checks for different data types.

Equality check for primitive data types like boolean , string and integer is very simple since they are always compared by their actual value:

1 === 1  ’string’ === ’string’  true === true

On the other hand, equality check for complex types like objects , arrays and_functions_ is completely different. Two objects are the same if they have the same reference (pointing to the same object in memory).

const obj1 = { prop: ’someValue’ };  const obj2 = { prop: ’someValue’ };  console.log(obj1 === obj2);   // false

Even though obj1 and obj2 appear to be the same, their reference is different. Since they are different, comparing them naively within the shouldComponentUpdate function will cause our component re-render needlessly.

The important thing to note is that the data coming from Redux reducers, if not set up correctly, will always be served with different reference which will cause component to re-render every time.

This is a core problem in our quest to avoid component re-rendering.

Handling References

Let’s take an example in which we have deeply nested objects and we want to compare it to its previous version. We could recursively loop through nested object props and compare each one, but obviously that would be extremely expensive and is out of the question.

That leaves us with only one solution, and that is to check the reference, but new problems emerge quickly:

  • Preserving the reference if nothing has changed
  • Changing reference if any of the nested object/array prop values changed

This is not an easy task if we want to do it in a nice, clean, and performance optimised way. 非死book realised this problem a long time ago and called Immutable.js to the rescue.

import { Map } from ‘immutable’;    //  transform object into immutable map  let obj1 = Map({ prop: ’someValue’ });    const obj2 = obj1;                                     console.log(obj1 === obj2);  // true    obj1 = obj1.set(‘prop’, ’someValue’);  // set same old value  console.log(obj1 === obj2);  // true | does not brake reference because nothing has changed     obj1 = obj1.set(‘prop’, ’someNewValue’);   // set new value  console.log(obj1 === obj2);  // false | brakes reference 

None of the Immutable.js functions perform direct mutation on the given data. Instead, data is cloned internally, mutated and if there were any changes new reference is returned. Otherwise it returns the initial reference. New reference must be set explicitly, like obj1 = obj1.set(...); .

React, Redux and Immutable.js

The best way to demonstrate the power ofthese libraries is to build a simple app. And what can be simpler than a todo app?

For brevity, in this article, we will only walk through the parts of the app that are critical to these concepts. The entire source code of the app code can be found on GitHub .

When the app is started you will notice that calls to console.log are conveniently placed in key areas to clearly show the amount of DOM re-render, which is minimal.

Like any other todo app, we want to show a list of todo items. When the user clicks on a todo item we will mark it as completed. Also we need a small input field on top to add new todos and on the bottom 3 filters which will allow the user to toggle between:

  • All
  • Completed
  • Active

Redux Reducer

All data in Redux application lives inside a single store object and we can look at the reducers as just a convenient way of splitting the store into smaller pieces that are easier to reason about. Since reducer is also a function, it too can be split into even smaller parts.

Our reducer will consist of 2 small parts:

  • todoList
  • activeFilter
// reducers/todos.js  import * as types from 'constants/ActionTypes';  // we can look at List/Map as immutable representation of JS Array/Object  import { List, Map } from 'immutable';    import { combineReducers } from 'redux';      function todoList(state = List(), action) {  // default state is empty List()    switch (action.type) {    case types.ADD_TODO:      return state.push(Map({   // Every switch/case must always return either immutable         id: action.id,          //  or primitive (like in activeFilter) state data          text: action.text,      //  We let Immutable decide if data has changed or not        isCompleted: false,      }));      // other cases...      default:      return state;    }  }      function activeFilter(state = 'all', action) {    switch (action.type) {    case types.CHANGE_FILTER:      return action.filter;  // This is primitive data so there’s no need to worry      default:      return state;    }  }    // combineReducers combines reducers into a single object  // it lets us create any number or combination of reducers to fit our case  export default combineReducers({    activeFilter,    todoList,  });

Connecting with Redux

Now that we have set up a Redux reducer with Immutable.js data, let’s connect it with React component to pass the data in.

// components/App.js  import { connect } from 'react-redux';  // ….component code  const mapStateToProps = state => ({       activeFilter: state.todos.activeFilter,      todoList: state.todos.todoList,  });    export default connect(mapStateToProps)(App);

In a perfect world, connect should be performed only on top level route components, extracting the data in mapStateToProps and the rest is basic React passing props to children. On large scale applications it tends to get hard to keep track of all the connections so we want to keep them to a minimum.

It is very important to note that state.todos is a regular JavaScript object returned from Redux combineReducers function (todos being the name of the reducer), but state.todos.todoList is an Immutable List and it is critical that it stays in such a form until it passes shouldComponentUpdate check.

Avoiding Component Re-render

Before we dig deeper, it is important to understand what type of data must be served to the component:

  • Primitive types of any kind
  • Object/array only in immutable form

Having these types of data allows us to shallowly compare the props that come into React components.

Next example shows how to diff the props in the simplest way possible:

$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual';    shouldComponentUpdate(nextProps, nextState) {    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);  }

Function shallowEqual will check the props/state diff only 1 level deep. It works extremely fast and is in perfect synergy with our immutable data. Having to write this shouldComponentUpdate in every component would be very inconvenient, but fortunately there is a simple solution.

Extract shouldComponentUpdate into a special separate component:

// components/PureComponent.js  import React from 'react';  import shallowEqual from 'react-pure-render/shallowEqual';    export default class PureComponent extends React.Component {    shouldComponentUpdate(nextProps, nextState) {      return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);    }  }

Then just extend any component in which this shouldComponentUpdate logic is desired:

// components/Todo.js  export default class Todo extends PureComponent  {    // Component code  }

This is a very clean and efficient way of avoiding component re-render in most cases, and later if the app gets more complex and suddenly requires custom solution it can be changed easily.

There is a slight problem when using PureComponent while passing functions as props. Since React, with ES6 class , does not automatically bind this to functions we have to do it manually. We can achieve this by doing one of the following:

  • use ES6 arrow function binding: <Component onClick={() => this.handleClick()} />
  • use bind : <Component onClick={this.handleClick.bind(this)} />

Both approaches will cause Component to re-render because different reference has been passed to onClick every time.

To work around this problem we can pre-bind functions in constructor method like so:

  constructor() {      super();      this.handleClick = this.handleClick.bind(this);    }   // Then simply pass the function    render() {      return <Component onClick={this.handleClick} />    }

If you find yourself pre-binding multiple functions most of the time, we can export and reuse small helper function:

// utils/bind-functions.js  export default function bindFunctions(functions) {    functions.forEach(f => this[f] = this[f].bind(this));  }  // some component    constructor() {      super();      bindFunctions.call(this, ['handleClick']);   // Second argument is array of function names    }

If none of the solutions work for you, you can always write shouldComponentUpdate conditions manually.

Handling Immutable Data Inside a Component

With the current immutable data setup, re-render has been avoided and we are left with immutable data inside a component’s props. There are number of ways to use this immutable data, but the most common mistake is to convert the data right away into plain JS using immutable toJS function.

Using toJS to deeply convert immutable data into plain JS negates the whole purpose of avoiding re-rendering because as expected, it is very slow and as such should be avoided. So how do we handle immutable data?

It needs to be used as is, that’s why Immutable API provides a wide variety of functions, map and get being most commonly used inside React component. todoList data structure coming from the Redux Reducer is an array of objects in immutable form, each object representing a single todo item:

[{    id: 1,    text: 'todo1',    isCompleted: false,  }, {    id: 2,    text: 'todo2',    isCompleted: false,  }]

Immutable.js API is very similar to regular JavaScript, so we would use todoList like any other array of objects. Map function proves best in most cases.

Inside a map callback we get todo , which is an object still in immutable form and we can safely pass it in Todo component.

// components/TodoList.js  render() {     return (        // ….              {todoList.map(todo => {                return (                  <Todo key={todo.get('id')}                      todo={todo}/>                );              })}        //  ….      );  }

If you plan on performing multiple chained iterations over immutable data like:

myMap.filter(somePred).sort(someComp)

… then it is very important to first convert it into Seq using toSeq and after iterations turn it back to desired form like:

myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()

Since Immutable.js never directly mutates given data, it always needs to make another copy of it, performing multiple iterations like this can be very expensive. Seq is lazy immutable sequence of data, meaning it will perform as few operations as possible to do its task while skipping creation of intermediate copies. Seq was built to be used this way.

Inside Todo component use get or getIn to get the props.

Simple enough right?

Well, what I realized is that a lot of the time it can get very unreadable having a large number of get() and especially getIn() . So I decided to find a sweet-spot between performance and readability and after some simple experiments I found out that Immutable.js toObject and toArray functions work very well.

These functions shallowly convert (1 level deep) Immutable.js objects/arrays into plain JavaScript objects/arrays. If we have any data deeply nested inside, they will remain in immutable form ready to be passed to components children and that is exactly what we need.

It is slower than get() just by a negligible margin, but looks a lot cleaner:

// components/Todo.js  render() {    const { id, text, isCompleted } = this.props.todo.toObject();    // …..  }

Let’s See It All in Action

In case you have not cloned the code from GitHub yet, now is a great time to do it:

git clone https://github.com/rogic89/ToDo-react-redux-immutable.git  cd ToDo-react-redux-immutable

Starting the server is as simple (make sure Node.js and NPM are installed) as this:

npm install  npm start

Navigate to http://localhost:3000 in your web browser. With the developer console open, watch the logs as you add a few todo items, mark them as done and change the filter:

  • Add 5 todo items
  • Change filter from ‘All’ to ‘Active’ and then back to ‘All’
    • No todo re-render, just filter change
  • Mark 2 todo items as completed
    • Two todos were re-rendered, but only one at a time
  • Change filter from ‘All’ to ‘Active’ and then back to ‘All’
    • Only 2 completed todo items were mounted/unmounted
    • Active ones were not re-rendered
  • Delete a single todo item from the middle of the list
    • Only the todo item removed was affected, others were not re-rendered

Wrap Up

The synergy of React, Redux and Immutable.js, when used right, offer some elegant solutions to many performance issues that are often encountered in large web applications. Immutable.js allows us to detect changes in JavaScript objects/arrays without resorting to the inefficiencies of deep equality checks, which in turn allows React to avoid expensive re-render operations when they are not required.

I hope you liked the article and find it useful in your future React endeavours.

About the author

Ivan Rogic, Croatia

member since August 15, 2015

CSS JavaScript React.js

Ivan first started coding back in 2007 at the beginning of his college education, and he became really passionate about it. He likes learning new technologies and staying on top of his game all the time. During his early employment, he learned a lot about the importance of communication between team members and how to be a great team player. [click to continue...]

</div> </div> </div>