Angular - Splitter and Aggregation patterns for ngrx/effects

I work on a large Angular app that uses @ngrx/store for our state management and @ngrx/effects for handling side effects. Most of the time we fire a single action and expect one effect to call an api endpoint, and update the store with the response. However, sometimes a user interaction requires us to wait for responses from more than one API endpoint before we update the store and the UI.

Effects in ngrx usually only react to single actions (via the .ofType operator), but if we send out two API calls we need an effect that reacts to two (success) actions. This raises a more fundamental design question: How can we orchestrate multiple actions (bring them in the right order, wait for all of them to finish, etc.) and after that continue with the remainder of the effect.

For example, consider we have an online clothing shop with various products like pants, t-shirts, shorts, etc. and we are viewing a product, say a t-shirt. Product information will include both information about the product such as color, size, etc. and about the shipping options. The product information and shipping information are available from two different APIs and we are unable to change the API so it returns all the information we need in a single API call.

For this example, we update the product summary in the store only when we have information about both the product and the shipping options. So we need to wait for two different API calls to be able to show the product summary.

The event flow would be:

User interaction happens

Effect makes the calls to the APIs

With the response from the APIs, the store is updated and feedback is given to the user

Different ways of solving the problem

GetProductInformationAction and GetShippingInformationAction are the 2 actions that make the API calls

ProductSelectors.getProductInformation() and ProductSelectors.getShippingInformation() provide the response from the APIs

GotProductSummaryAction updates the store with the product and shipping information for the given product

Solution 1: Chained Dispatch of Actions

We could solve it by having:

an effect which listens to GetProductSummaryAction and dispatches GetProductInformationAction which makes the first API call

an effect which listens to GetProductInformationAction and dispatches GetShippingInformationAction which makes the second API call

and finally an effect which listens to GetShippingInformationAction and dispatches GotProductSummaryAction which updates the store with information about both the product and shipping.

Drawbacks

The 2 API calls can be made simultaneously, but since the effects are chained here, the API calls are not made in parallel which means that the user has to wait even longer.

The actions cannot be reused in another context because they are part of a chain and using them would always trigger the chain.

Solution 2: One effect to do it all

We could have a single effect which dispatches the subsequent actions, gets the necessary information and updates the store like below:

Drawbacks

We are selecting the information from the store without any checks on whether the store is updated due to GetProductSummaryAction. Because, GetProductInformationAction and GetShippingInformationAction are quite generic, meaning they could have been triggered by other processes in the application too and could contain stale data.

Also, GetProductSummaryAction could have been triggered multiple times and we do not have any logic to find out the actions and store updates that are correlated.

This makes testing a pain, because there is a lot going on here and therefore it makes the test setup hard.

It is best to avoid dispatching actions from an effect chain and instead emit an action directly, thereby breaking big complex effects down into smaller linked ones. This makes effects/observable chains a lot easier to reason about, and also easier to test.

Solution 3: Final solution

Victor Savkin did a really good talk about different patterns for ngrx/effects at Angular Connect 2017, let’s look at a couple of those patterns that are important in solving this problem.

Splitter pattern

The Splitter pattern is where an action is mapped to an array of actions.

Let us start by breaking down the above effect and only dispatching GetProductInformationAction and GetShippingInformationAction in it. It would now look like:

Aggregation pattern

The Aggregator pattern is where an array of actions map back into a single action.

Let us say, upon successful API call to get product information, we dispatch GotProductInformationAction and similarly for shipping information we dispatch GotShippingInformationAction.

To complete our chain of actions, we wait for GotProductInformationAction and GotShippingInformationAction, aggregate them both and then dispatch GotProductSummaryAction.

Applying the splitter and aggregation pattern, our action flow would look like below:

Linking the correlated actions

Since GetProductInformationAction and GetShippingInformationAction are quite generic, we are only interested in the ones that are related to GetProductSummaryAction.

Also, since GetProductSummaryAction could have been triggered multiple times, we only want to get the subsequent actions that are related to the particular GetProductSummaryAction. Therefore we need some form of ids to match these actions. To achieve this, when we dispatch the GetProductSummaryAction with a Correlation Id.

So the effect would now look something like below:

In case of any errors when we try to get either product information or the shipping information from the API, we dispatch GetAggregatedProductInformationFailAction .

Now, we need to somehow know when these actions are finished and all of the information is available, so we can proceed further.

The magic operator that aggregates

Let us create a magic operator that does the above for us and let us name it aggregate.

The operator should:

take in the two actions that we need to wait for to aggregate and a failure action to break the chain in case of failure

filter the actions (the actions to aggregate and the failure action) that match the correlation parameters of the parent action

forkJointhe actions so we wait for both actions to emit and aggregate them together

throw an error observable and stop the chain, in case of a failure action

stop the chain when either the actions to be aggregated are emitted or when the failure action is emitted, so it should race them both and return the first value emitted

This can also be extended to take in more than 2 actions, but most of the time, you would probably need only 2 or 3 actions to be aggregated.

Final effect

Using the above operator in our effect, it would like this:

In the above effect, we are waiting for all the co-related actions to finish up the sequence of actions. The co-related actions here are: GotProductInformationAction, GotShippingInformationAction & GetAggregatedProductInformationFailAction.

Why do we need a fail action?

We have GetAggregatedProductInformationFailAction because we do not want any memory leaks caused when neither GotProductInformationAction nor GotShippingInformationAction are emitted due to network connectivity issues or API issues causing the filter in the operator to wait infinitely for the actions matching the correlation parameters.

So we either get GotProductInformationAction and GotShippingInformationAction or a GetAggregatedProductInformationFailAction.

When the aggregated actions are available, GotProductSummaryAction will be dispatched and in case of failure, GetProductSummaryFailAction will be dispatched.

Conclusion

Always try to keep the effects simple and avoid dispatching actions inside them. All effects should be returning side effect actions but not dispatching side effect actions inside them. Eliminating complications in the effects makes it easier to understand, maintain and test. Splitter and Aggregation pattern are only two of the different patterns you could follow to craft the effects nicely. Check this talk out for more patterns and techniques on writing the effects.

Thank you for reading! If you enjoyed this article, please feel free to 👏 and help others find it. Please do not hesitate to share your thoughts in the comments section below. Follow me on Medium or Twitter for more articles. Happy coding folks!! 💻