The principles and patterns behind NgRx/Redux are nothing new. They are originating from CQRS (command query responsibility segregation) and event-driven systems. In that context, there are different ways to communicate an event to the actors in the system.
This post will shed some light on what the different types of NgRx actions are and how to choose the right action type for the given use case.
Note, this post is inspired by Victor’s post about NgRx best practices and is extending upon the usage of actions in NgRx.
The most recommended way to use actions is as unique events as they make it easy to trace where the action is dispatched from. These kinds of actions are unique events that represent something that has happened in the app eg.
This is beneficial for scalability and one to many communication as the action can be handled by many reducers/effects as it is not implying any specific usage. Also, this is mentioned in the Redux style guide as the recommended way to use actions (instead of setter/commands) as it provides these benefits:
- A more meaningful action name
- Fewer total dispatched actions
- A more meaningful action log history
As a general rule of thumb, you only want one action to be dispatched in every function. An exception to this rule is when you also need to dispatch a document message (a dispatched event and a dispatched document message), more about this in the next section.
As we see here, the action is dispatched when the button is clicked and the naming is displaying that as well.
This can then be used in multiple reducers and effects:
This will make it easy to see what event the action is coming from and scales well to be used in multiple reducers/effects.
But sometimes we will need to use a more reusable action to represent a change in an entity, especially as an interface to synchronize changes between feature modules when entities change. This takes us to…
These kinds of messages are representing that a given entity has been updated. These are normally used to synchronize changes/behavior between entities. Eg. in a banking app, you do a transfer and want to let the app know the affected account(s) changed so the app can update the state accordingly. Such an action might be named so it reflects something got updated eg.
accountsChanged with the corresponding entity id(s) in the action payload.
This is beneficial for many to one/many communication as the action can be dispatched from every usage that is updating a given entity and the effect would normally be to update the updated entity’s state, eg. in a banking app, you complete a transfer and want to have it reflected in the transaction list for the affected accounts.
This kind of action is ideal for synchronizing changes to a given entity and let subscribers know if and how a given entity has been changed, so they can act accordingly. Especially in a big enterprise setup (feature team per feature module), this is a good interface to have between feature teams to coordinate entity changes without being coupled to other team’s code.
Commands are when we are explicit about how the action should be handled which makes for a more imperative control flow. The good thing about imperative flows is they are fast and simple to work with initially low scale but they don’t scale very well.
Since reactive architectures are recommended for scalable and maintainable Angular apps, we need to be careful not to overdo this. Some even recommend not using actions as commands, ever. I personally still think an action as a command can be beneficial in this scenario: You want to trigger one specific effect (eg. to load some data,
loadTransactionsRequest) and the action should *never* do anything else than that. And, if it starts to do other things you will need to convert the action to an event. If you don’t agree on this, it is totally fine just going with actions as events all the way. Now you are warned, you should just be aware that using actions as commands will compromise the reactive control flow. Done too much, you might as well drop NgRx and use state properties in service.
Commands can be used for many to one communication as the action is explicit about the usage context thus is implying what it will do. That means, this action can be called from many places in the app.
Let’s consider how an “appropriate” use case for a command (calling just an HTTP request through an effect + setting loading state).
Other use cases for commands could be logging requests.
Again, notice this kind of action will ruin the reactive control flow, will be hard to trace the origin event that triggered it and will be a pain to maintain if done too much. Now you are warned! I am not going to be black and white and tell you to NEVER use this kind of action but I still think the mentioned example is a valid use case.
General Redux action best practices
You can read all the official Redux best practices in the style guide here but I want to elaborate on some best practices related to the topic of the blog post.
Only dispatch one action per function/method
The reasoning behind this is to limit the number of actions to keep the control flow event-based/reactive by letting the actions (as unique events) control the control flow. Sometimes you might BOTH dispatch an event and a document message.
Use actions as events, use multiple actions for the same reducer, effect
A natural consequence of using actions as events, you will have multiple events resulting in the same state change or side effect resulting in you needing to use multiple actions in reducers and effects – that is completely fine.
Use actions as events, use action in multiple reducers
Likewise, when using actions as events, you often need to update state in multiple reducers when a given event has happened. This is also fine and is recommended by the Redux style guide.
How to structure this in a scaled-up NgRx setup
I have experience with being a tech lead/coach for huge projects with over 50 developers working on the same app. A key to success on this kind of scale is ensuring team autonomy by splitting the teams’ responsibilities up among different feature areas, ideally a feature module per team. It is important to have clear interfaces/agreements on how to communicate among feature areas.
I recommend trying this kind of setup:
- You use actions as events internally in your feature area
- You create a *feature-area-name*-public.actions.ts file in the root of your feature folder, containing your public actions, that other feature teams are allowed to subscribe to (in reducers and effects). These actions will mostly be document messages, as you normally want to synchronize the change in entities among feature teams (eg. reload/update new data)
That way, you create good encapsulation in the feature teams, ensuring they can work efficiently (as the number 1 productivity blocker is depending on external resources) as well as creating a clean contract for which actions other teams can react to and dispatch using the public action file.
We went through the different ways to use NgRx actions: events, document messages and commands, and when to use what. Also, we saw this in the context of Redux’s best practices and how to use this in a big Angular project.
This is part of the NgRx training in Angular Architect Accelerator so if you are looking to master NgRx and learn all how to apply the best practices for your specific use cases as a tech lead, I recommend you check out the course or at least join the free warmup workshop.