Using Immutable Data Structures to prevent excessive React renders

Published

For an brief introduction to the concepts behind Immutable Data Structures, and a few code examples in JavaScript, see my article here.

The first place to start when trying to reduce the number of renders performed by a React Component is to define an effective shouldComponentUpdate() function. Within this function we can determine if the data pertinent to the Component in question truly has changed. This is useful if multiple Components share data from the same source (e.g. Flux Store or parent Component props).

Immutable JS, and immutable data structures in general, allow us to use JavaScript’s simple equality operators when trying to detect changes in our source data (i.e. comparing the Component’s previous props/state with the current version). Using the equality operator is simple, quick, and can be consistently used across all data changed checks.

In our example below, we will be creating a Reflux Store to house GPS routes that are received from a WebSocket (however the source of the routes is not important). Once the routes are received by the Store, they will be added to an Immutable JS Map, which will then be emitted out to all listening Components. Our example features one Component that will use logic in its shouldComponentUpdate() function to determine if new routes need to be added to the OpenLayers map.

Configuring the Data Source

Below is the definition of our Reflux Store. We will be using the Immutable JS Map Object to house our routes (defined on line 19). We could also use the Immutable JS List, however Map allows us to get and set at a particular key (similar to native JavaScript Objects vs. Arrays).

In our example, the Flux Store will be updated with new data each time a WebSocket message is received from the backend (however the Store could of course be updated based on any logic, including when Flux Action calls are placed). We define the WebSocket connection and assign the event handler function on lines 23 and 26 below.

Within the event handler function (lines 30-52), the WebSocket message from the backend is parsed into proper OpenLayers Features. Once that is complete, the Features are added to the routes Map via the set() function (we are using the route ID as the key).

Notice how the set() function on line 43 returns a new Object, which we are storing in the routes variable. This new Object contains everything that was in the routes Map originally, but also contains the new Features we just added. This reflects the proper process for updating immutable data structure described above. Thankfully the Immutable JS library takes care of all of this logic for us behind the scenes (and includes optimizations for performing these “updates”).

The new routes are then emitted out to listening Components on lines 48-50.

import Reflux from 'reflux';

import io from 'socket.io-client';
import ol from 'openlayers';

import Actions from '../actions/actions.js';

const { Map } = require('immutable');

class RouteStore extends Reflux.Store {

    constructor() {

        //listenables
        this.listenables = Actions;

        //set default app state
        this.state = {
            routes: new Map()
        };

        //initialize WebSocket Connection to backend
        var socket = io();

        //attach event listener to handle receipt of new routes
        socket.on( 'FeatureCollection' , this.appendNewRoute.bind(this) );

    }

    appendNewRoute(routeMessage) {

        //define OpenLayers GeoJson parse; can be memoised to function or saved
        //  to store to prevent re-creating each time!
        var geoJsonParser = new ol.format.GeoJSON();

        //parse route features into OpenLayers feature
        var routeFeatures = geoJsonParser.readFeatures( routeMessage , { featureProjection: 'EPSG:3857' } );
        var routeId = routeFeatures[0].get('routeSequence') ;

        //save new route features into Immutable JS Map object in this.state.routes
        //  - performed by using the Map.set() function
        //  - notice the .set() function returns a new object, which we then save into the routes variable
        var routes = this.state.routes.set( routeId , routeFeatures );

        //emit updated routes out to all listening components - reminder: the updated routes Object is a NEW
        //  and different Object than before; this allows us to use an equaliy check to detect if the 
        //  routes Object has been changed in our Component's shouldComponentUpdate()
        this.setState({
            routes: routes
        });

    }

}

module.exports = RouteStore;

Configuring the Component

Our example Component is defined below. Line 15 of the shouldComponentUpdate() function checks if the routes Object in our previous state equals the routes Object in the new state. We know that when Immutable JS makes an update to one of its data structures (i.e. Map, List, etc.), it will return a new and different Object. Hence if the Objects are not equal, we know the underlying data has been updated, and we need to update our Component!

The componentDidUpdate function defined on lines 18-32 shows an example of how you can load data from within a React Component into an external library, or OpenLayers 3 in this case. See this article for more details on how to perform integration between React Flux environments and external libraries.

//externals
import Reflux from 'reflux';
import ReactDOM from 'react-dom';
import React from 'react';

//open layers and styles
var ol = require('openlayers');
require('openlayers/css/ol.css');

class Map extends Reflux.Component {
 
  //other functions eliminated for brevity

  shouldComponentUpdate(nextProps, nextState) {
   return prevState.routes !== this.state.routes;
  }

 componentDidUpdate(prevProps, prevState) {

    // set new route Features to and OpenLayers Vector Layer (assume the Vector
   //  Layer was been created in componentDidMount, and a reference to the Layer
   //  Object has been saved into state)
   this.state.routesLayer.setSource(
     new ol.source.Vector({
        //in this case we need to flatten the routes Map Object 
        //  into a native JavaScript Array
        features: [].concat.apply( [], this.state.routes.valueSeq().toArray() ), 
       wrapX: false
      })
    );

  }

 //other functions eliminated for brevity

}

module.exports = Map;

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *