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.
- 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:
- 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
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:
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
- forkJoin the 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.
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.
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.