简体   繁体   中英

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 ie

# 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...

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.

It sounds like a combination of the following discussions on github could help you.

store.getItem() which may require an async server call: https://github.com/facebook/flux/issues/60

Managing amount of client-side data: https://github.com/facebook/flux/issues/62

Essentially getting the store data is synchronous, the component could then tell the store to do the long running task but then forgets about it.

Once the task is completed in the store an action is created and the flow thing happens at which time the component can synchronously get the required information from the store.

Does that make sense?

If I was going to create an async process in the most Flux way possible, I would approach it much like an XHR request -- kick off the async process in either the Action Creator or the Store (whichever makes the most sense for the app) and then call a new Action Creator to dispatch a new action when the async process completes. This way, multiple stores can respond to the completed expensive async process, and the data flow is still emanating from an Action.

You could also just add a handler to your Store that gets called when a certain event is emited in your store

So lets say in your store you have a method:

Store = { ...

   addUnreadDoneListener:function(callback){
      this.on(SOME_EVENT_CONSTANT, callback);
   },

...}

In your componentWillMount you can register to this "addUnreadDoneListener" with a function of your component, which then gets called everytime your store emits this certain event.

I personally do this aswell in my project. And I think its pretty easy to manage this way

Hope this helped.

EDIT: I forgot to mension... I use Eventemitter to do this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM