The three types of actions in NgRx

Share on facebook
Share on google
Share on twitter
Share on linkedin

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.

Event

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

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…

Document message

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.

Command

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.

Conclusion

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.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How to do Cypress component testing for Angular apps with MSW

In this post, we will cover how to do Cypress Component testing with MSW (mock service worker) and why it’s beneficial to have a mock environment with MSW. The mock environment My recommendation for most enterprise projects is to have a mocking environment as it serves the following purposes : * The front end can

Read More »

Handling Authentication with Supabase, Analog and tRPC

In this video, I cover how to handle authentication with Supabase, Analog and tRPC. It’s based on my Angular Global Summit talk about the SPARTAN stack you can find on my blog as well. Code snippets Create the auth client Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Read More »