Ngrx: How to simplify the definition of actions?

Viewed 414

Is there any way to simplify the definition of actions so that we don't need to repeat code over and over again?

Problem

Working with Ngrx we have to work with actions all the time. In my situation, the most time each action does have a "Do"-Action, a "Success"-Action and a "Failure"-Action. In my situation each of these actions does have the same scope (e.g. "[Authentication]") and the same label (e.g. "Login user").

When I want to create actions for user authentication I would create these as follows:

export const loginUser = createAction(
    '[Authentication] DO: Login user',
    props<{
      username: string;
      password: string;
    }
);

export const loginUserSuccess = createAction(
    '[Authentication] SUCCESS: Login user',
    props<{
      token: string;
    }
);

export const loginUserFailure = createAction(
    '[Authentication] FAILURE: Login user',
    props<{
      error: string;
    }
);

As you can see there is a lot of repetition:

  • three "[Authentication]" for the same scope
  • three "Login user" for the same kind of action
  • the "DO", "SUCCESS", "FAILURE" parts are the same in all actions

Is there any way to create a factory that simplifies the action creation with less redundant code?

My current solution

See my answer to this post.

I created a package ngrx-simple with the goal of simplifying ngrx development. It implements a class SimpleActions that helps to group actions and reduce code repetition:

https://github.com/julianpoemp/ngrx-simple


My old solution

(the code in the new package is better)

The only simplification I found so far is creating a class of actions of the same scope and wrap all actions for the same kind of action in an object:

store.functions.ts

import {createAction, props} from '@ngrx/store';

export function createDoActionType(scope: string, label: string) {
  return `[${scope}] DO: ${label}`;
}

export function createSuccessActionType(scope: string, label: string) {
  return `[${scope}] SUCCESS: ${label}`;
}

export function createErrorAction(scope: string, label: string) {
  return createAction(
    `[${scope}] FAILURE: ${label}`,
    props<ErrorProps>()
  );
}

export interface ErrorProps {
  error: string;
}

authentication.actions.ts

export class AuthenticationActions {
  private static scope = 'Authentication';

    static loginUser = ((scope: string, label: string) => ({
    do: createAction(
      createDoActionType(scope, label),
      props<{
        userEmail: string;
        password: string;
      }>()
    ),
    success: createAction(
      createSuccessActionType(scope, label),
      props<{
        userID: number;
        token: string;
      }>()
    ),
    failure: createErrorAction(scope, label)
  }))(AuthenticationActions.scope, 'Login user');
}

I see my solution as workaround only. Although it saves me 10 lines of code, it isn't optimal...

2 Answers

My company has open sourced a lib just to handle this as an extension to ngrx.

https://github.com/acandylevey/ngrx-http-tracking

With createTrackingActions, 3 actions are created:

  • loading -> the request
  • loaded
  • failure
const actionNameSpace = 'Users';

(...)

//                                         Request type     Response Type
//                                                \/          \/   
export const fetchUsers = createTrackingActions<UserRequest, User[]>(
  actionNameSpace,
  'fetchUsers'
);

maried with it we have the createTrackingEffect function which takes an actions created by createTrackingActions and hooks up the 3 underlining actions

import { UserApiService } from './user-api.service';
import * as UserActions from './user.actions';

(...)

fetchUsers$ = createTrackingEffect(
    this.actions$,
    UserActions.fetchUsers,
    this.userApi.fetchUsers,
    'Could not load users' // there is a final optional function argument here to allow further function calls after the action is processed.
  );

And the reducer is also simplified, as usually only the loaded action needs to be handled:

const usersReducer = createReducer(
  initialState,
  on(UserActions.fetchUsers.loaded, (state, { payload }) =>
    userssAdapter.setAll(payload, { ...state })
  )
);

Tracking the results

By default, all actions are tagged with a global tag that can then be easily intercepted a global level, to provide global error handling or a global loading spinner if any actions are waiting on http requests.

httpTrackingFacade is also provided by the lib

isLoaded$ = this.httpTracker.isLoaded(UserActions.fetchUsers);
 isLoading$ = this.httpTracker.isLoading(UserActions.fetchUsers);

 isGloballyLoading$ = this.httpTrackingFacade.getGlobalLoading();
 globalErrors$ = this.httpTrackingFacade.getGlobalErrors();

and to track individual actions There is

Firing the request from a facade

 fetchUsers() {
    this.store.dispatch(UserActions.fetchUsers.loading());
    return UserActions.fetchUsers;
  }

Tracking it

  this.httpTrackingFacade
      .getResolved(this.userFacade.fetchUsers())
      .subscribe((result) => {
        console.log(`The result from the fetch users call was: {result}`);
      });

Ngrx offers a function called createActionGroup. It allows to create a set of actions with the same context ("called source).

The following example creates a set of actions for a group called "click":

import {createActionGroup} from "@ngrx/store";

export const click = createActionGroup({
  source: 'button/click',
  events: {
      do: props<TestProps>(),
      success: emptyProps(),
      fail: props<TestProps>()
  }
});

Now we can dispatch the actions as common:

this.store.dispatch(click.do({test: "ok"});
this.store.dispatch(click.success());
this.store.dispatch(click.fail({test: "ok"});

This dispatches actions with following types:

[button/click] do
[button/click] success
[button/click] fail

In my case there is still need for improvements like a method that creates only the actions "do, success, fail" without the need to set props (no props = emptyProps() automatically). But this is good for now.

Related