Search code examples
javascriptreactjsreactjs-flux

How do you manage expensive derived calculations in a "flux" application


I am currently working on a prototype application using the flux pattern commonly associated with ReactJS.

In the Facebook flux/chat example, there are two stores, ThreadStore and UnreadThreadStore. The latter presents a getAll method which reads the content of the former synchronously.

We have encountered a problem in that operations in our derived store would be too expensive to perform synchronously, and would ideally be delegated to an asynchronous process (web worker, server trip), and we are wondering how to go about solving this.

My co-worker suggests returning a promise from the getter i.e.

# MyView

componentDidMount: function () {
    defaultState = { results: [] };
    this.setState(defaultState);
    DerivedStore.doExpensiveThing()
        .then(this.setState);
}

I'm not entirely comfortable with this. It feels like a break with the pattern, as the view is the primary recipient of change, not the store. Here's an alternative avenue we've been exploring - in which the view mounting event dispatches a desire for the derived data to be refreshed (if required).

 # DerivedStore
 # =========================================================
 state: {
     derivedResults: []
     status: empty <fresh|pending|empty>
 },
 handleViewAction: function (payload) {
    if (payload.type === "refreshDerivedData") {
        this.state.status = "pending"; # assume an async action has started
    }
    if (payload.type === "derivedDataIsRefreshed") {
        this.state.status = "fresh"; # the async action has completed
    }
    this.state.derivedResults = payload.results || []
    this.notify();
 }

 # MyAction
 # =========================================================
 MyAction = function (dispatcher) {
    dispatcher.register(function (payload) {
        switch (payload) {
            case "refreshDerivedData": 
               doExpensiveCalculation()
                   .then(function(res) {
                        dispatcher.dispatch({
                            type: "derivedDataIsRefreshed",
                            results: res
                        })
                    })
               );
        }
    });
 };

 # MyView
 # =========================================================
 MyView = React.createClass({
     componentDidMount: function () {
         if (DerivedStore.getState().status === "empty") {
             Dispatcher.dispatch("refreshDerivedData");
         }
     },
     getVisibility: function () {
         return DerivedStore.getState().status === "pending" ? "is-visible" : ""
     },
     render: function () {
         var state = DerivedStore.getState()
             , cx = React.addons.classSet
             , classes = cx({
                "spinner-is-visible": state.status === "pending"
             });

         return <div {classes}>
                   <Spinner /> # only visible if "spinner-is-visible
                   <Results results={state.derivedResults}/> # only visible if not...
                </div>;
     }

 });


 # MyService
 # =========================================================

 # ensure derived data is invalidated by updates in it's source?
 OriginalStore.addListener(function () {
     setTimeout(function () {
        dispatcher.dispatch({
            type: "refreshDerivedData"
        })
     }, 0); 

 });

What I like about this approach is that the view treats the DerivedStore as it's view model, and views of this ilk are primarily interested in the freshness of their view model. What concerns me however is the potential for stores coming out of sync.

My question(s) then:

  • is the promise approach acceptable?
  • is the second approach better/worse? If so, why?
  • is there an existing "canonical" approach to this problem?

PS: sorry if there are any fundamental linting errors in this code, I've been working in Coffeescript for the last 3 months and it's destroyed my linting powers...


Solution

  • All async actions should be caused by the creation of an action. The completion of an async action should be signaled by the creation of another action. Stores may listen to these actions, and emit a change event.

    In your component you listen to a DerivedStore for changes. An action can be created from anywhere, such as in your component or another store. The data is (eventually) derived, the store is updated, a change event is emitted, and your component(s) apply the event payload to state.

    All in all, your component doesn't actually know if what's happening behind the scenes is sync or async. This is great because it allows you to make these performance changes behind the scenes without risk of breaking your components.

    Pure stores usually only have one public function which gets the state of the store. In your components you should only call this in getInitialState, or better yet: have a mixin which does this and adds the change listener for you.