06 Oct 2022
5 min

What’s new in NgRx? Changes overview, tips, and tricks.

I assume you have already heard about State Management – how it is used, why we should use it, and what it can help us with. In this article, let’s dive into NgRx – we will review new features and ways to make it easier to work with.

Actions

Whoever used state management from NgRx, knows well what actions are and what they are used for. The principle of operations remains the same between versions, so what then has changed in the actions themselves? First of all, the syntax, as in all of NgRx 🙂 At this point, we have two ways to create actions.

The first one uses the createAction method:

export const login = createAction(
  '[Login Page] Login'
  props<{ payload: LoginPayload }>()
);

It is simpler in comparison to the previous way that was a class with a constructor.

export class Login implements Action {
  readonly type = '[Login Page] Login'

  constructor(public payload: LoginPayload){}
}

Less boilerplate and looks much nicer, although it is not a perfect way. You probably experienced situations when you created new actions using the copy-paste method and forgot to change the action type that must be unique. Otherwise, it can cause an unexpected result, for example, a double call to API. It is where the brand-new createActionGroup method comes to our rescue:

const authApiActions = createActionGroup({
  source: 'Auth API',
  events: {
    'Login': props<{ payload: LoginPayload }>
    'Login Success': props<{ userId: number; token: string; }>(),
    'Login Failure': props<{ error: string; }>(),
  },
});

This is the solution to our problems: events are the record, our type is the key and we can’t give two stocks with the same key. What a miracle, right?

concatLatestFrom 

Until now, if we wanted to extract data from the state in effect, we would have used the withLatestFrom operator. However, currently, NgRx Team recommends using a new one – concatLatestFrom. What is the difference between the two?

While pulling data from the state, we sometimes encounter a situation when they are outdated. Then it occurs to us that something must be happening too fast which is in our case pulling data from the state.

In WithLatestFrom, the subscription was “eager,” meaning that it could listen for an action even before it was executed. In the new concatLatestFrom operator, the subscription is “lazy,” so the action will start listening only after it is executed. ConcatLatestFrom eliminates this error and retrieves data only after the action has been despatched.

Entities

A good practice in data management is storing them as keyed objects, i.e. a map. The world became a better place, when NgRx Team created a solution that provides us with this out of the box. All you have to do is use @ngrx/entity. Using @ngrx/entity to store data also gives us a benefit in terms of data reading speed. The computational complexity of the map data retrieval operation is O(1) compared to O(n) for an array.

The state created with the entity looks like this:

export interface State extends EntityState<User> {
  selectedUserId: string | null;
}

export const adapter: EntityAdapter<User> = createEntityAdapter<User>({
  selectId: (user: User) => user.userId
});

export const initialState: State = adapter.getInitialState({
  selectedUserId: null,
})

We extend our state interface by EntityState and create our initial state using entityAdapter.

The result is a created state that, in addition to the fields we declared, has additional fields: entites and ids. Entities are a map of the objects we want to hold in it. By default, the key is the id props. We can change it, obviously. In this case, selectUserId will be our key. Why do I mention the default key and the possibility of changing it? Let’s assume that our user model looks like this:

export interface User {
  userId: number;
  name: string;
  surname: string;
}

In this case, if we set the data as it comes from the API to our state, we will see that the data was not set correctly.

In entites, the first key is undefined precisely because we have not set another key for the map. In this case, we need to either map our data and add an additional prop (id) to it or change the default entites key from id in our case to userId. 

What exactly is this adapter? Well, he adapter provides us with methods to modify entities. That is, with its help we can add something to entities, remove or modify something.

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, (state, { users }) => {
    return adapter.setAll(users, state);
  }),
  on(UserActions.updateUser, (state, { update }) => {
    return adapter.updateOne(update, state);
  }),
  on(UserActions.deleteUser, (state, { id }) => {
    return adapter.removeOne(id, state);
  }),
);

Selectors

Selectors with props are deprecated at this point. This is a simple example of what a selector with props looked like, and how it should look like at this point.

// correct
export const getCount = (multiply: number) => createSelector( getCounterValue, (counter) => counter * multiply );

// depricated
export const getCount = createSelector( getCounterValue, (counter, props) => counter * props.multiply );

The recommended way of calling selectors has also changed. Now it looks much friendlier.

// use that 
this.store.select(selectUser());

// instead of
this.store.pipe(select(selectUser));

NgRx and standalone components

The 14th version of Angular brought quite a few changes, including the long-awaited standalone approach. At this point, we can create our application without using modules. How then to declare reducers and effects? 

If we want to declare our state or effect on a global level we can do it using the importProviderFrom method in the application injector. 

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      StoreModule.forRoot({
        router: routerReducer,
        auth: authReducer,
      }),
      StoreRouterConnectingModule.forRoot(),
      StoreDevtoolsModule.instrument(),
      EffectsModule.forRoot([RouterEffects, AuthEffects])
    ),
  ],
});

NgRx has also prepared its dedicated methods for importing state and effect: provideEffect and provideStore.

bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ router: routerReducer, auth: AuthReducer }),
    provideRouterStore(),
    provideStoreDevtools(),
    provideEffects([RouterEffects, AuthEffects]),
  ]),
});

The import substitute forFeature is just as simple. State and effects we can provide in routing.

path: '',
providers: [
  provideStoreFeature('users', usersReducer),
  provideFeatureEffects([UsersApiEffects]),
],
children: [
...
]

To sum up, over the years, NgRx has given us quite a few new solutions and  improvements that make the work of us, developers easier. There is no doubt that NgRx is the most popular and most common state management in angular projects, so it is worth keeping up to date, updating packages consistently and checking out our blog, where we regularly inform you about tips and news that are released.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.