DEV Community

spyke
spyke

Posted on

Pitfalls of Flux Dispatcher

Flux was presented in May 2014 and quickly become a new movement in web development. Today Flux isn't that widely used. The driver seat was taken by its offspring Redux. Anyway, it's still interesting to discuss some of the issues with Flux's architecture about which you don't even think in Redux.

This one was famous:

"Cannot dispatch in the middle of a dispatch"

This error had to mean that you did dispatch() in a wrong time and need to move it somewhere else. The most brave people just ignored it by wrapping the dispatch into setTimeout(). But there were many other hacks to avoid it.

Flux's official website and issue tracker have no good explanation of how to deal with this problem only recommending not to dispatch. Unfortunately, there're too many scenarios when it's unavoidable. As you'll see later this error is only a symptom of a much larger issue.

Flux describes a store as a state manager of a domain. That means you will have more stores than 1. Same time, some stores may depend on another, what is described by calling waitFor() method.

Imagine a basic app of two components:

<App>
    <Posts />
</App>
Enter fullscreen mode Exit fullscreen mode

The App is the root and shows a login screen instead of its children while user isn't authenticated. The Posts component starts loading its data in componentDidMount() hook what is the recommended practice. Both these components depend on different stores: AppStore and PostsStore. The PostsStore may also depend on the AppStore too, but it isn't important.

Let's look at the time when user just authenticated and the app got a positive answer from the server with user's session:

Auth Flow Diagram

Actions are represented as arrow-like blocks. Let's follow the diagram:

  1. AUTH_SUCCESS is dispatched. Flux Dispatcher starts calling stores' callbacks and does this in order.
  2. AppStore's callback is called first, the store recalculates its state.
  3. All AppStore subscribers start to update. We have only one subscriber in our case  -- the  App component.
  4. The state was updated, and the App starts to re-render.
  5. This time isAuth is true and we start rendering Posts (this happens synchronously).
  6. componentDidMount() also happens synchronously. So, just right after the initial Posts render we start loading actual posts (Posts shows a Loader).
  7. Loading posts means dispatching LOAD_POSTS_STARTED first.
  8. What means we're back in the Flux Dispatcher, which will throw the nasty error.

Now look at the #5. When render happens we're still in the middle of dispatch. That means that only a part of stores were updated and we're looking at inconsistent state. Not only we're getting errors in totally normal scenarios, but even without errors the situation is hardly better.

The most popular solution to this entire scope of issues is to fire change event in setTimeout(). But this removes synchronicity of React rendering. Theoretically, event subscribers may be called in different order, because order of execution of setTimeout callbacks is unspecified (even if we know that browsers just add them to a queue).

I like another solution which isn't that well known, but lies on surface. Redux works this way and is consistent, error-less, and synchronous. The whole dispatch process inside Redux may be written as such:

dispatch(action) {
    this.$state = this.$reducer(this.$state, action);
    this.$emit();
}
Enter fullscreen mode Exit fullscreen mode

It calculates the new state and only then calls subscribers. The state is always consistent, and the entire process is like an atomic DB transaction.

In Flux this approach would be more verbose, but still doable. Stores manage their subscribers individually, but they could return a function to dispatcher. This function will call store's emit(). Most of the time stores don't pass event arguments, so they would just return the emit itself. In case if you want to optimize some things and filter events based on args, a store may return a custom callback.

Taking Flux Dispatcher as the base only a few places require tweaks:

dispatch(payload){
    // No more "Cannot dispatch..."
    this._startDispatching(payload);
    // Same try/finally as before.
    // After state calculation notify all subscribers.
    this._notifyAll();
}

_notifyAll() {
    // In case of a nested dispatch just ignore.
    // The topmost call will handle all notifications.
    if (!this._isNotifying) {
        this._isNotifying = true;
        while (this._notifyQueue.length > 0) {
            const notify = this._notifyQueue.shift();
            notify();
        }
        this._isNotifying = false;
    }
}

_invokeCallback(id) {
    this._isPending[id] = true;
    // Save callback from the store to the queue.
    const notify = this._callbacks[id](this._pendingPayload);
    if (notify) {
        this._notifyQueue.push(notify);
    }
    this._isHandled[id] = true;
}
Enter fullscreen mode Exit fullscreen mode

It requires some error handling code, but the idea should be clear. This is how a store may look like:

class PostsStore extends EventEmitter {
    constructor(dispatcher) {
        this.$emit = this.$emit.bind(this);
        this.$posts = {};
        this.dispatchToken = dispatcher.register(payload => {
            switch (payload.actionType) {
                case "LOAD_POSTS_SUCCESS":
                    // Don't forget to "return" here
                    return this.$loadPosts(payload);
            }
        };
    }

    $loadPosts(payload) {
        this.$posts[payload.userId] = payload.posts;
        return this.$emit; // A generic case with no args;
    }

    $clearPosts(userId) {
        delete this.$posts[userId];
        // When only a part of subscribers should update.
        return () => this.$emit(userId);
    }
}
Enter fullscreen mode Exit fullscreen mode

The rest of the app's code stays the same.

This solution has not that big refactoring penalty, but gives you state consistency, removes unnecessary errors, and keeps update-render process synchronous and simple to follow.

Atomicity is a nice property which we hadn't in Flux and not always notice in Redux. Redux is also more simple, maybe that's why we (the community) haven't seen implementations like Atomic Flux Dispatcher and moved forward straight to Redux.

Originally posted on Medium in 2019.

Top comments (0)