NgRx: Action Creators redesigned

Alex Okrushko
Angular In Depth
Published in
8 min readApr 8, 2019
Trimming Action Creators to the bare minimum that we actually need

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

In this article, we’ll look into the limitations of the current Action Creators and some techniques that can help with them. Then I’ll walk you through the new addition to the core NgRx — createAction function, where I’ll highlight some of its advantages, how it can be used in ofType operator, and discuss Action Union.

Background

Actions are a core part of NgRx, or as some say

check out the thread 👆to learn more about what Actions are

They are what glues the entire state management together.

However, over the years of maintaining NgRx at Google I’ve heard many times that Actions:

  • Feel heavy and require quite a bit of boilerplate
  • Are hard to track where they are consumed at and where they are dispatched from

What’s so hard about tracking the Actions? Let’s start by taking a look at the current Action Creators.

Action Creators

Action Creators are special functions or classes that help remove some of the boilerplate (Tim Deschryver described them very well in his article). They, however, also introduce a disconnect:

  • Reducers and Effects rely on type, which is typically either an enum or a string constant:
    In a Reducer:switch (action.type) { case ACTION_TYPE_STRING: {…} }
    or in an Effect: ofType(ActionEnum.ACTION_TYPE)
  • Components or results in Effects use the Action Creator itself,
    new MyAction() for a class-based approach or myAction() for a function-based.

Let’s take a look at the Action Creator example from ngrx.io:

The Reducer will use ActionTypes.Login in the switch statement, which is an enum value in this case.
At Firebase Console (and mostly at all of Google) we settled on constant strings for types, and hence the Login Action would have the following shape:

As you can see, that’s a lot of code for something that is supposed to be unique and not reusable.

On top of that, enums or strings do not allow us to find all the places the Action is used throughout the codebase. For consumption, the enum/string has to be used — and for dispatching, we need to use the class.

It got to a point where our intern Idan Raiter created a tool that walks down AST, maps the Action dispatchers (Components and Effects) and consumers (Effects and Reducers) and visualizes their relationships in d3 graphs.

Visualization for example-app that is packaged with NgRx

Another approach that simplified tracking Actions is “Good Action Hygiene” (a term coined by Mike Ryan). It recommends using Actions strictly as unique events and adjusting Action types to include their sources explicitly, which makes the stream of Actions a lot more readable in DevTools.

Good Action Hygiene helps a lot, but only if the application is already running and it still doesn’t address the problem of searching for Actions in the codebase.

Better NgRx

Internally at Firebase, Moshe Kolodny started a doc, where he outlined some of the improvements to NgRx that he thought could help make state management a bit less painful. One of such ideas was to adjust the Action’s class prototype and to include type as part of it. Users won’t be forced to use enum/string for the type, and instead something like Login.prototype.type can be accepted instead.

I really liked the idea and some iterations later I managed to get to Login.type potential usage — it was addressing the searchability issue and reducing some boilerplate. This is how the initial proposal was drafted (that was eventually modified to its current state).

While I was working on the implementation, I stumbled upon ts-action — the Action Creators library by Nicholas Jamieson, and he further pointed me to the article here in Angular-in-Depth that covers this library very well (and that I somehow missed). It had everything that I drafted, and on top of that, it was solving the problem of properties/payload boilerplate, and basically addressing most of the complaints that I’ve heard about Actions.

After discussing it with the NgRx team, we decided to merge the ts-action Action Creator code into core NgRx, with some adjustments.

Improved Action Creators

The ergonomics of the new Action Creators are quite pleasant to work with. Starting from NgRx version 7.4.0 you can re-implement the Login Action from above:

This Action feels a lot more light-weight and concise. The createAction function has some resemblance to createSelector and is quite easy to read.

Here is how this Action Creator is used:
login({username: 'Tim', password: 'NgRx'})

Now, in the Reducers and Effects we will use the same login function. This function has the type property attached to it, so login.type is all we need:

Searching for all the usages is also a breeze, look at this example, where I’m looking for the loadCollection Action:

Obsolete Payload

When I write Actions I’d like to be explicit about which properties get argument values (when there is more than a single property), so I pass named arguments:
new Login({username: 'Tim', password: 'NgRx'})

However, classes cannot destructure the passed object and assign it to properties in the constructor itself — this feature was requested a long time ago for Typescript. To bypass this limitation, the payload is typically used:
constructor(readonly payload: LoginPayload) {}

Wrapping properties in the extra payload, in turn, would require us to unwrap it where the Action is used:
service.loginUser(action.payload.username, action.payload.password)

This was an inconvenience I was willing to go with to get the named arguments, but something that I never really enjoyed doing.

With the new Action Creator, payload could become a thing of the past — a
props() function takes care of adding properties without payload!
service.loginUser(action.username, action.password)

Should you want to use the payload with the new Action Creators — you can still do that:

What if I want to assign default values? Is it possible for me to pass the arguments without naming them?

Yes, it is possible. The Action Creator can also take the Creator function as a second parameter:

Now we can create Actions with login('Brandon') or login('Mike', 'lessSecurePassword').

Comparison between class-based Action Creator and new createAction function

ofType in Effects

When we specify which Actions need to be handled by the Effect, we use the custom ofType operator and pass in the type as a string.

However, we can take it even further and pass just the Action Creator itself:

ofType(login.type) // <-- still works
ofType(login) // <-- reads even better

Mixing strings in Action Creators are also possible, here is the example from the spec file:
ofType(divide, 'ADD', square, 'SUBTRACT', multiply)

When used with strings, in order to provide typing correctly, ofType relies on the Actions union to be provided for the Actions class in the constructor. Then it narrows it down to the specific Action:

That’s another advantage that the Action Creator brings — it doesn’t need that generic.

ofType that takes Action Creator is released with NgRx version8.0.0.

createReducer

With Action Creator in place and ofType adjusted to handle it better, the team focused on implementing a createReducer function, instead of going through a switch statements.

Comparison between switch-statement-based reducer and createReducer function

The new function takes the initialState as the first parameter, which has to be typed properly (e.i. object literal should not be used) and then any number of on functions. In their turn, each of them takes up to ten Action Creators and the reducer function that has to return new state, similar to what each case statement does.

Reducer functions with each on function can has two arguments:

  • state
  • action (or actions intersection, when multiple actions are provided)

Frequently actions would be deconstructed to extract the properties of the action, e.g. action for loadBook is deconstructed into { book }.

Actions Union

With ofType and createReducer handling new Action Creators, there is no need for the Actions Unions unless Action Creators are used in the previous style reducers.

In the Redux pattern (that NgRx follows) ALL dispatched Actions go through ALL Reducers and Effects.

any dispatched action gets into Actions stream and goes through all reducers and all effects

Yet, it’s impossible to combine the types of all of these Actions into one single type. That’s why both Actions<Action> in Effects and function reducer(state, action: Action), use the Action interface, that has type: string.

On the other hand, we want auto-completion and type checking. And for that to work, we need to pretend that we are working only with a limited subset of Actions — this is why we are creating the Actions Union type that we pass to our Reducers and Effects.

In the section ofType in Effects I explained how new Action Creators eliminate the need to provide a union to the Actions generic. But we still might need it for switch-statement-based reducers if you didn’t convert all of them to createReducer.

To create a union with one or two Actions, I recommend just combining them manually:
export type AuthApiActionsUnion = ReturnType<typeof loginSuccess | typeof loginFailure>;
When creating the union of 3+ Actions, there’s a helper union function:

const all = union({login, loginSuccess, loginFailure, logout});
export type LoginActionsUnion = typeof all;

Unfortunately, the export cannot be combined into a single statement because TypeScript doesn’t support it.

Conclusion

The new Action Creator ticks all the boxes for me: it feels lighter, more concise, improves searchability and readability, helps to avoid payload and makes state management simpler.
NgRx’s example-app has already been updated with the new Action Creators, so don’t forget to check it out.

Are you interested in NgRx? Do you want to learn everything from the basics to advanced techniques?
If you are in San Francisco / Bay Area, I’ll be doing the popular 2-day NgRx workshop on October 23–24, 2019. Tickets and more info is available here: https://www.eventbrite.ca/e/ngrx-in-san-francisco-from-start-to-advanced-concepts-in-2-days-tickets-74313759455?aff=aid
Let me help you take your state management skills to the new heights! Hope to see you there!

--

--

Angular In Depth
Angular In Depth

Published in Angular In Depth

The place where advanced Angular concepts are explained

Alex Okrushko
Alex Okrushko

Written by Alex Okrushko

Software Engineer at Google working on Firebase Console.

Responses (13)