Streamlining Action Definitions in Ngrx

Is there a way to streamline action definitions to eliminate repetitive code?

Challenge

When working with Ngrx, actions are constantly needed. Typically, each action consists of a "Do", "Success", and "Failure" variation. These actions often share the same scope (e.g. "[Authentication]") and label (e.g. "Login user").

For example, creating authentication actions involves significant repetition:

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;
    }
);

The redundancy includes:

  • three instances of "[Authentication]" for the same scope
  • three occurrences of "Login user" for the same type of action
  • repeating the "DO", "SUCCESS", "FAILURE" components in every action

Is there a method to simplify action creation with less duplication?

My Current Approach

Refer to my response below.

I developed a package ngrx-simple aiming to streamline ngrx development. It introduces a class SimpleActions that facilitates action grouping and reduces redundancy:

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


Previous Solution

(the new package code is preferred)

The only simplification I discovered involved creating a set of actions within the same scope and encapsulating all actions of a similar type within 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');
}

This workaround, while saving lines of code, is not considered optimal...

Answer №1

Our company has generously open sourced a library specifically designed to streamline the process within ngrx.

Check out our repository here

Through the use of createTrackingActions, three distinct actions are automatically generated:

  • loading -> indicates the request is in progress
  • loaded -> signals successful completion
  • failure -> denotes an error occurred
To illustrate, consider the following example:

const actionNameSpace = 'Users';

(...)

export const fetchUsers = createTrackingActions<UserRequest, User[]>(
  actionNameSpace,
  'fetchUsers'
);

Additionally, there is a convenient function called createTrackingEffect available which pairs with actions created by createTrackingActions to seamlessly integrate the three underlying actions.

For instance:

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'
  );

The reducer implementation is also straightforward, typically only requiring handling of the loaded action:

Here's an example:

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

Monitoring and Tracking Results

Every action defaults to being labeled with a global tag, facilitating easy interception at a universal level for tasks such as global error handling or displaying a loading indicator while awaiting http requests.

The library even supplies a handy tool called httpTrackingFacade

You can utilize it like so:

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

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

Furthermore, granular tracking of individual actions is supported

Initiating a Request via a Facade

To send a request through a facade, you can do the following:

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

Keeping Tabs on it

To keep track of the request status, you can employ the following method:

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

Answer №2

The Ngrx library provides a useful function known as createActionGroup, which allows you to generate a collection of actions sharing the same context (referred to as source).

For instance, the code snippet below demonstrates how to create a set of actions for a group named "click":

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

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

You can now dispatch these actions in the following manner:

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

These dispatches correspond to the following action types:

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

In my scenario, there is room for improvement such as implementing a method that generates actions "do, success, fail" without requiring explicit props (automatically defaulting to emptyProps()). However, the current functionality suffices for now.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Hamburger Menu in Bootstrap not functioning as expected

Despite following each step meticulously for implementing the Hamburger menu in the navbar, I encountered an issue. The navbar collapses when resizing the window but fails to open upon clicking it. Below is a sample code snippet for reference. It utilizes ...

Liferay's JavaScript bundler does not automatically remove unused node modules

Within my work on Liferay 7.3.5 and developing an Angular Portlet, I have encountered a frustrating issue. When experimenting with different modules or versions of the same module, I noticed that the final jar OSGI module contains all the modules I have in ...

What are some strategies I can implement to effectively manage different errors and ensure the app does not crash

I came across a variety of solutions for error handling. The key concept revolves around this explanation: https://angular.io/api/core/ErrorHandler Attempts were made to implement it in order to capture TypeError, however, the outcome was not successful: ...

Ways to implement pointer event styling within a span element

Hi, I'm having trouble with styling and I can't seem to figure out how to resolve it. The style pointer-events: none doesn't seem to be working for me. Here is an example of my code: The style snippet: .noclick { cursor: default; ...

Exploring nested keys within JSON data using TypeScript

I am currently working with this object structure { "Monday": [ { "morning": [ { "start_time": "02:00", "end_time": "07:3 ...

Data loss occurs when the function malfunctions

Currently, I am working with Angular version 11. In my project, I am utilizing a function from a service to fetch data from an API and display it in a table that I created using the ng generate @angular/material:table command. Client Model export interfac ...

Exploring Angular 4: Understanding the nuances between search and params when using http get with parameters

When working with Angular 4's HTTP package ('@angular/http'), there is an option to pass a URLSearchParams object in the get request. What sets apart using search versus params when assigning the parameters object in the request method? For ...

What are the best ways to improve the efficiency of my filtering function

Currently, I'm working on a project involving Angular, NestJS, GraphQL, and MongoDB. I have created a modal component for filtering data that contains multiple fields. However, I am not confident that the code I wrote follows best practices and am see ...

Adding a tooltip to an Angular Material stepper step is a great way to provide

I am trying to implement tooltips on hover for each step in a stepper component. However, I have encountered an issue where the matTooltip directive does not seem to work with the mat-step element. <mat-horizontal-stepper #stepper> <mat-step lab ...

Disable the ng2-tooltip-directive tooltip when the mouse is moved

Is there a way to hide the tooltip when the mouse enters? How can I achieve this? <div (mousemove)="closeTooltip()" [tooltip]="TooltipComponent" content-type="template" show-delay="500" placement= ...

Sending information to an array of child components within Angular

In my project, I am working with a parent component and child components. The parent component has an array of objects that are called from an API. Parent Component public Questions = []; ngOnInit(): void { this.loadQuestions(); } <div *ngIf=&quo ...

Challenges encountered when setting a value to a custom directive via property binding

I'm working on a question.component.html template where I render different options for a specific question. The goal is to change the background color of an option to yellow if the user selects the correct answer, and to red if they choose incorrectly ...

Despite encountering the 'property x does not exist on type y' error for Mongoose, it continues to function properly

When working with Mongoose, I encountered the error TS2339: Property 'highTemp' does not exist on type 'Location' when using dot notation (model.attribute). Interestingly, the code still functions as intended. I found a solution in the ...

Unwrapping the Angular ngForm Mystery: Resolving

I was attempting to retrieve values using ngOnInit and initializing the form with default values, but for some reason the form is returning as undefined. I also tried using patchValue, but since the form is undefined, it doesn't work. It's intere ...

Imitate a HTTP request

Currently, I am working on developing a front-end application using Angular (although not crucial to this question). I have a service set up that currently supplies hard-coded JSON data. import { Injectable } from '@angular/core'; import { Obser ...

Unit testing in Angular - creating mock services with observables

I'm currently facing an issue with my unit tests for a component that subscribes to an Observable from the service DataService in the ngOnInit() lifecycle hook. Despite my efforts, I keep encountering the error message TypeError: Cannot read propertie ...

Asyncronous calls in Angular involve executing tasks without

The issue seems to be related to the timing of updates for the controlSelected and isAssessmentDataLoading variables. The updateQuestions() method is invoked within the ngOnInit() method, which is triggered when the component is initialized. However, the ...

Implementing reCaptcha on React Native: A Step-by-Step Guide

Currently, I am in the process of integrating a reCaptcha validator into a login screen for a react-native application that needs to function seamlessly on both web and mobile platforms. Despite being relatively new to programming and lacking experience w ...

Submit a pdf file created with html2pdf to an S3 bucket using form data

Currently, I have the following script: exportPDF(id) { const options = { filename: 'INV' + id + '.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, dpi: 300, letterRendering: true, useC ...

Incorporating a React Bootstrap spinner into your project

I am looking to enhance my modal by adding a spinner to it. Here is the current structure of my modal: <Modal show={modal.show} onHide={onHideModal}> <form onSubmit={onImport}> <Modal.Header closeButton> <Mo ...