Simon Krueger

Batch Updating in React

This post talks about how React components perform updates for different types of events and how we can get these updates to behave in a way that is ideal when using AJAX actions that update multiple Flux stores.

SyntheticEvents

React components will batch their updates inside of Events. This means that when a component's setState function is called multiple times inside of an OnClick event, the component will only call render once (if shouldComponentUpdate returns true). This is exactly the behavior I want. I only want to interact with stores that are in a complete, stable, and ready state.

AJAX and timeout events

Unfortunately, AJAX and window.timeout events are not batched like SyntheticEvents. It is common for my code to update multiple stores from a single AJAX response. Bugs crop up by using the default behavior of re-rendering on each call to setState. Luckily, there is a pattern to batch these updates and it is pretty simple to do to. I batch the updates by using a single store that sets a isLoading boolean at the beginning of the AJAX event call and doesn't clear it until all the stores have finished their updates. The main react component will use this isLoading boolean inside of its shouldComponentUpdate function. The logic will say to not update if the current state and the next state is loading. This isLoading acts as a lock and batches all the stores updates into one single render call. This holds the invariant in our components that stores will be in a complete and stable state during render calls.

UI Lock Example

Here is a partially implemented version of a loading lock that performs batch updates for AJAX events.

AppDispatcher.dispatch({
  type: 'AJAX_ACTION',
});

_isLoading = false;
var AppStore = {
  isLoading() {
    return _isLoading;
  },
};

AppStore.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION':
      _isLoading = true;
      AppStore.emitChange();
      break;

    case 'AJAX_ACTION_SUCCESS':
      AppDispatcher.waitFor([OtherStore1.dispatchToken, OtherStore2.dispatchToken]);
      _isLoading = false;
      AppStore.emitChange();
      break;
  }
});

var OtherStore1 = {
  // Store state...
};

OtherStore1.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION_SUCCESS':
      // Update state
      OtherStore1.emitChange();
      break;
  }
});

var OtherStore2 = {
  // Store state...
};

OtherStore2.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION_SUCCESS':
      // Update state
      OtherStore2.emitChange();
      break;
  }
});

function getState() {
  return {
    storeState1: OtherStore1.getAll(),
    storeState2: OtherStore2.getAll(),
    isLoading: AppStore.isLoading(),
  };
}

var App = Reat.createClass({
  shouldComponentUpdate(nextProps, nextState) {
    var update = true;
    if (this.state.isLoading && nextState.isLoading) {
      update = false;
    }

    return update;
  },

  render() {
    // Stores are in a stable complete state invariant holds! \o/
  },
});