ptheodosiou
ptheodosiou

Reputation: 400

Is there a way to dispatch an action once?

Details

I've been working on a project where I'm getting data from an API. In order to fetch these data, I store what I need in the store. For example, in the store, I'm storing the currently selected period, the currently bbox of a map and the data the is being fetched from the API. What I need to achieve is to update the data in the store every time there is a change in the selected period or the current bounding box of the map.

For this, I've created an Action, a Reducer and an Effect for Periods, BBox and Stats. The logic that I'm following is below and I'm using it for BBox and Stats too.

Code

export interface IPeriodState {
  periods: Period[];
  isLoading: boolean;
  error: Error;
}

In reducers, I just mutate the state and nothing more. My actions are in the same logic at this one:

import { Action } from "@ngrx/store";
import { Period } from "../../models/Period";

export enum PeriodActionTypes {
  ADD_PERIOD = "[Period] Add Period",
  ADD_PERIOD_SUCCESS = "[Period] Add Period Success",
  ADD_PERIOD_FAILURE = "[Period] Add Period Failure",

  REMOVE_PERIOD = "[Period] Remove Period",
  REMOVE_PERIOD_SUCCESS = "[Period] Remove Period Success",
  REMOVE_PERIOD_FAILURE = "[Period] Remove Period Failure",

  UPDATE_PERIOD = "[Period] Update Period",
  UPDATE_PERIOD_SUCCESS = "[Period] Update Period Success",
  UPDATE_PERIOD_FAILURE = "[Period] Update Period Failure"
}

// Add Period
export class AddPeriodAction implements Action {
  readonly type = PeriodActionTypes.ADD_PERIOD;
  constructor(public payload: Period) {}
}

export class AddPeriodSuccessAction implements Action {
  readonly type = PeriodActionTypes.ADD_PERIOD_SUCCESS;
  constructor(public payload: Period) {}
}

export class AddPeriodFailureAction implements Action {
  readonly type = PeriodActionTypes.ADD_PERIOD_FAILURE;
  constructor(public error: Error) {}
}

// Remove Period
export class RemovePeriodAction implements Action {
  readonly type = PeriodActionTypes.REMOVE_PERIOD;
  constructor(public id: string) {}
}

export class RemovePeriodSuccessAction implements Action {
  readonly type = PeriodActionTypes.REMOVE_PERIOD_SUCCESS;
  constructor(public id: string) {}
}

export class RemovePeriodFailureAction implements Action {
  readonly type = PeriodActionTypes.REMOVE_PERIOD_FAILURE;
  constructor(public error: Error) {}
}

// Update Period
export class UpdatePeriodAction implements Action {
  readonly type = PeriodActionTypes.UPDATE_PERIOD;
  constructor(public id: string, public payload: Period) {}
}

export class UpdatePeriodSuccessAction implements Action {
  readonly type = PeriodActionTypes.UPDATE_PERIOD_SUCCESS;
  constructor(public id: string, public payload: Period) {}
}

export class UpdatePeriodFailureAction implements Action {
  readonly type = PeriodActionTypes.UPDATE_PERIOD_FAILURE;
  constructor(public error: Error) {}
}

export type PeriodActions =
  | AddPeriodAction
  | AddPeriodSuccessAction
  | AddPeriodFailureAction
  | RemovePeriodAction
  | RemovePeriodSuccessAction
  | RemovePeriodFailureAction
  | UpdatePeriodAction
  | UpdatePeriodSuccessAction
  | UpdatePeriodFailureAction;

In effects now, I'm dispatching multiple actions. For example:

//imports omitted

@Injectable()
export class PeriodEffect {
  constructor(private actions: Actions) {}

  @Effect() addPeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.ADD_PERIOD),
    mergeMap((action: AddPeriodAction) => [
      new AddPeriodSuccessAction(action.payload),
      new LoadStatsAction()
    ]),
    catchError(error => of(new AddPeriodFailureAction(error)))
  );

  @Effect() removePeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.REMOVE_PERIOD),
    mergeMap((action: RemovePeriodAction) => [
      new RemovePeriodSuccessAction(action.id),
      new LoadStatsAction()
    ]),
    catchError(error => of(new RemovePeriodFailureAction(error)))
  );

  @Effect() updatePeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.UPDATE_PERIOD),
    mergeMap((action: UpdatePeriodAction) => [
      new UpdatePeriodSuccessAction(action.id, action.payload),
      new LoadStatsAction()
    ]),
    catchError(error => of(new UpdatePeriodFailureAction(error)))
  );
}

Problem

My problem is that I got the LoadStatsAction dispatched multiple times because I need to get the new data in the store, every time that there is a change in the website. Let's take a look at my period-selector component. Once there is a change in the period, I manage this change with the below code:

constructor(private moment: MomentPipe, private store: Store<AppState>) {
  this.currentPeriod = new Period(
    id(),
    new Date(2017, 9, 1),
    new Date(2018, 9, 1)
  );

  this.store.dispatch(new AddPeriodAction(this.currentPeriod));
}

ngOnInit() {}

ngOnDestroy() {
  this.store.dispatch(new RemovePeriodAction(this.currentPeriod.id));
}

updateCurrentPeriod(period: IPeriod) {
    this.currentPeriod._minDate = period.minDate;
    this.currentPeriod._maxDate = period.maxDate;
    this.store.dispatch(
      new UpdatePeriodAction(this.currentPeriod.id, this.currentPeriod)
    );
  }

Summary

Is there a way to get rid of the multiple dispatched actions?

Note

You can find the full code of the project here and a demo of the project here

Upvotes: 1

Views: 948

Answers (2)

timdeschryver
timdeschryver

Reputation: 15497

If I've the question right, I think the question is that you WANT to dispatch the actions more than once but you don't want to add it in the 3 effects? If that's the case you can do the following:

We can listen to multiple actions inside the ofType operator, use switchMap or exhaustMap to cancel multiple requests when the current request has not fulfilled yet.

  @Effect() addPeriod$ = this.actions.pipe(
    ofType(PeriodActionTypes.ADD_PERIOD_SUCCESS, PeriodActionTypes.REMOVE_PERIOD_SUCCESS, UPDATE_PERIOD_SUCCESS),
    switchMap((action) => {
       // logic here to load stats
       // or to dispatch the LoadStatsAction action
    }),
  );

Upvotes: 1

Tal Ohana
Tal Ohana

Reputation: 1138

I would recommend of extracting the logic of when to dispatch a LoadStatsAction action to avoid repetitive code, it should look like this:

  @Effect() addPeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.ADD_PERIOD),
    map((action: AddPeriodAction) => new AddPeriodSuccessAction(action.payload)),
    catchError(error => of(new AddPeriodFailureAction(error)))
  );

  @Effect() removePeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.REMOVE_PERIOD),
    map((action: RemovePeriodAction) => new RemovePeriodSuccessAction(action.id)),
    catchError(error => of(new RemovePeriodFailureAction(error)))
  );

  @Effect() updatePeriod$ = this.actions.pipe(
    ofType<PeriodActions>(PeriodActionTypes.UPDATE_PERIOD),
    map((action: UpdatePeriodAction) => new UpdatePeriodSuccessAction(action.id, action.payload)),
    catchError(error => of(new UpdatePeriodFailureAction(error)))
  );

  @Effect() loadStats$ = this.actions.pipe(
      ofType(
        AddPeriodSuccessAction,
        RemovePeriodSuccessAction,
        UpdatePeriodSuccessAction
      ),
      mapTo(new LoadStatsAction())
  )

Now you can focus on reducing the times LoadStatsAction is dispatched, use debounceTime(x) or throttleTime(x) (a distinction between these operators can be found here)

Upvotes: 3

Related Questions