I think it’s time you and I talk a bit about action creators. But before we get into action creators let’s first start with actions, define what they are and why they are needed.
In a NgRx (and Redux) application one of the first rules you learn is that the store state is read only. But how does one modify the state, you may ask. The answer to that question is through actions and reducers.
In order to update to the store state we have to send messages from the application to the store. These messages are called actions, you can compare an action as a transaction record. It represents something that took place in the application and can be considered a statement of fact. The reducers in the store are eagerly waiting for an action to pass through so it can create the new store state.
NgRx introduces Action Creators in version 7.4.0, allowing us to create a typed action by using
To create the action creator, we define the action’s type as the first parameter and use the second parameter to define additional action properties. We don’t need to define an action’s enum as this is accessible via the type property, for example
orderFood.type. For more info about this change see Alex Okrushko's post NgRx: Action creators redesigned.
type property and it has an optional payload. In fact you can define the shape of the action yourself, the only enforcement is that it must have a
type property and its value must be a string. Other than that the structure of the action can be shaped to your needs, although, I would encourage you to use a
payload property to add extra data to the action.
With these type values Redux Devtools provides an event log of everything that has happened inside your application.
An example of an action with and without payload looks as follows:
In a typical NgRx application you will probably see these action types defined as
enums. Doing this has the benefit to typecheck the action types, this is done because a typo is always one keystroke away and can be the cause of frustration for several minutes.
The refactored version of the actions above becomes:
SIDE NOTE: Instead of using an ActionTypes
enumit’s also a possibility to use constants to define the actions, as in
const ORDER_FOOD = ‘[Order Page] ORDER FOOD’. The behavior will be the same, so feel free to use your preferred way.
Now that we know what an action is, we have to send it to the store. To do this we use the
dispatch function which exists on the store.
Dispatching an action like this may seem OK at first sight but has the downside that you have to type out the action every time. You have to define the type, you have to import the
ActionType enum, and you have to define the payload. Speaking of the payload, you have to remember the structure of the payload and if it changes in the future you’ll have to modify it throughout your whole codebase.
Here is where action creators come into play.
Like the name already gives away, an action creator creates an action. This can be done in multiple ways, in this post we’ll cover the two most frequently used ones.
The class has a
readonly type property and via its constructor it gets the payload of the action, which can (and must) be typed.
If we just create the action object from the constructor’s input, we can shorten this as:
Notice that the payload is set as a readonly property in the constructor.
In order to dispatch the action we have to create a new instance of the class:
The function returns the action object based on the function’s input and sets the type.
To dispatch the action we call the function:
Personally, while this may not be the de facto way, this is my preferred way. Because using a factory function prevents common or easily made mistakes, especially if you’re new to NgRx or Redux in general.
One of these mistakes is that the action must be serializable. While a factory function doesn’t fully prevent this, it’s preventing some constructs which can be created or are even encouraged with classes.
If you think this is the only reason to use action creators, well… you’re wrong. An action creator also has some extra benefits.
The view layers of the application don’t need to know about action types, these are encapsulated inside the action creators. The only places where you would use action types are in the action creators (to create the action object), inside the reducer functions (to know when and how to modify the state) and inside the effects (to invoke side effects).
Unlike reducers, an action creator doesn’t need to be pure. The inside of an action creator is the place where you could add impure logic, e.g. adding the current time to the action with
Date.now(), generating a UUID, etc. A small tip that I would give here, is to make these properties overridable which would help you out when you’re writing tests, for example:
Which is used in the application as follows:
And in the tests where you need more control, you can override these values:
Inside action creators you can make calculations on the input instead of just returning the action object.
An action creator helps you and your team to remember the data which is needed to create an action via its input. This input can be transformed into the payload property of the action.
Creating the “right” payload structure has the advantage of tightening up your reducer. If we play it smart we can handle several actions at once by using the fall-through functionality of a switch statement:
In comparison to:
You only need to write the action creator unit test(s) once, in contrast to testing the action in every component where it would be used. A unit test can be as simple as:
By adding a small (but well known) abstraction layer into the NgRx flow, we end up with cleaner code suited for reusability. With the result of making it ourselves and our team easier to scale and maintain an application.
This is because:
- We remove boilerplate, although this may not seem like it
- We create a place for impure logic and calculations
- We make our tests less prone to break due to changes
- Common and easy-to-make mistakes when you’re new to NgRx
- Keeping browser tabs in sync using localStorage, NgRx, and RxJS
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.