ngneat / effects

🪄 A framework-agnostic RxJS effects implementation
https://www.netbasal.com
MIT License
63 stars 11 forks source link

[Angular] actions.dispatch() fired twice #32

Closed guillaumemaka closed 2 years ago

guillaumemaka commented 2 years ago

HI, i have an issue when I dispatch action, the action is dispatch twice.

Here how I declare actions and effects

actions.ts

import { actionsFactory, props } from '@ngneat/effects';
import { Downtime } from 'src/app/core/domain/models/downtime';
import { DowntimesFilter } from './downtimes.state';
import { CreateDowntimeDTO } from './models/create-downtime.dto';
import { DeleteDowntineDTO } from './models/delete-downtine-dto';
import { EditDowntimeDTO } from './models/edit-downtime.dto';
import { UpdateDowntineDTO } from './models/update-downtine-dto';

const actions = actionsFactory('downtimes');

export const setDowntimes = actions.create(
  'SET_DOWNTIMES',
  props<{ downtimes: Array<Downtime> }>()
);

export const filterDowntimes = actions.create(
  'FILTER_DOWNTIMES',
  props<{ filter: DowntimesFilter }>()
);

export const setLoading = actions.create(
  'SET_LOADING',
  props<{ loading: boolean }>()
);

export const editDowntime = actions.create(
  'EDIT_DOWNTIME',
  props<{ downtime: EditDowntimeDTO }>()
);

export const updateDowntime = actions.create(
  'UPDATE_DOWNTIME',
  props<{ downtime: UpdateDowntineDTO }>()
);

export const createDowntime = actions.create(
  'CREATE_DOWNTIME',
  props<{ downtime: CreateDowntimeDTO }>()
);

export const newDowntime = actions.create('NEW_DOWNTIME');

export const deleteDowntime = actions.create(
  'DELETE_DOWNTIME',
  props<{ downtime: DeleteDowntineDTO }>()
);

export const loadDowntimes = actions.create(
  'LOAD_DOWNTIMES',
  props<{
    client: string;
    plant: string;
    startTimestamp: number;
    endTimestamp: number;
    nodeId: string;
  }>()
);

export const loadWorkUnitsForDowntime = actions.create(
  'LOAD_WORKUNIT_FOR_DOWNTIME',
  props<{
    client: string;
    plant: string;
    nodeId: string;
  }>()
);

effects.ts

import { Injectable } from '@angular/core';
import { createEffect, ofType } from '@ngneat/effects';
import { map, switchMap, tap } from 'rxjs';
import { Downtime } from 'src/app/core/domain/models/downtime';
import { ApplicationErrorCode, applicationErrors } from '../../errors';
import { ConsoleLogger } from '../../logger/console/console-logger.service';
import { configure, debug } from '../../rxjs/operators/debug';
import { ODataApiService } from '../../services/odata-api/odata-api.service';
import { GetMachineBreakdownsBetweenTimePeriodOutput } from '../../services/odata-api/outputs/get-machine-breakdowns-between-time-period-output';
import { GetNodeAndImmediateChildrenOutput } from '../../services/odata-api/outputs/get-node-and-immediate-children-output';
import * as messagesActs from '../messages/messages.actions';
import { ERROR, NOTIFICATION, SUCCESS } from '../messages/types';
import * as acts from './downtimes.actions';
import { DowntimeRepository } from './downtimes.repository';
import { CreateDowntimeDTO } from './models/create-downtime.dto';
import { DeleteDowntineDTO } from './models/delete-downtine-dto';
import { NewDowntimeDTO } from './models/new-downtime.dto';
import { NodeDetails, NodeType } from './models/node-details';
import { UpdateDowntineDTO } from './models/update-downtine-dto';

@Injectable({
  providedIn: 'root',
})
export class DowntimesEffects {
  constructor(
    private downtimeRepo: DowntimeRepository,
    private oDataService: ODataApiService,
    private logger: ConsoleLogger
  ) {
    configure(this.logger.level, this.logger.logWithDate);
  }

  loadDowntimes$ = createEffect((actions) =>
    actions.pipe(
      ofType(acts.loadDowntimes),
      switchMap((payload) =>
        this.oDataService.getMachineBreakdownsBetweenTimePeriod({
          client: payload.client,
          plant: payload.plant,
          nodeId: payload.nodeId,
          startTimestamp: payload.startTimestamp.toString(),
          endTimestamp: payload.endTimestamp.toString(),
          plantTimezoneOffset: 0,
        })
      ),
      debug('DowntimesEffects::loadDowntimes'),
      map((res) => {
        this.downtimeRepo.setDowntimes(toDowntimes(res));
      })
    )
  );

  loadWorkUnitsForDowntime$ = createEffect(
    (actions) =>
      actions.pipe(
        ofType(acts.loadWorkUnitsForDowntime),
        // if I put a debug() here it log 4 times
        switchMap((payload) =>
          this.oDataService.getNodeAndImmediateChildren({
            client: payload.client,
            plant: payload.plant,
            nodeId: payload.nodeId,
          })
        ),
        // Log output x2
        debug('DowntimesEffects::loadWorkUnitsForDowntime'),
        tap((res) => {
          this.downtimeRepo.setWorkUnitForDowntime(toNodeDetails(res));
        })
      ),
    { dispatch: false } // <---- true doesn't change anything
  );

  newDowntime$ = createEffect(
    (actions) =>
      actions.pipe(
        ofType(acts.newDowntime),
        debug('DowntimesEffects::newDowntime'),
        map(() => this.downtimeRepo.setDowntimeDTO(new NewDowntimeDTO()))
      ),
    { dispatch: false }
  );

  createDowntime$ = createEffect(
    (actions) =>
      actions.pipe(
        ofType(acts.createDowntime),
        debug('DowntimesEffects::createDowntime'),
        switchMap((payload: { downtime: CreateDowntimeDTO }) =>
          this.oDataService.reportMultipleDowntime({
            client: payload.downtime.client,
            plant: payload.downtime.plant,
            nodeID: payload.downtime.nodeID,
            downStartEndList: [payload.downtime],
          })
        ),
        map((res) => {
          return res.d.outputCode === 0
            ? messagesActs.showMessage({
                category: SUCCESS,
                messageOrError: 'Downtime added successfuly!',
                showAs: NOTIFICATION,
              })
            : messagesActs.showMessage({
                category: ERROR,
                messageOrError:
                  applicationErrors[ApplicationErrorCode.CannotCreateDowntime],
                showAs: NOTIFICATION,
              });
        })
      ),
    { dispatch: true }
  );

  editDowntime$ = createEffect((actions) =>
    actions.pipe(
      ofType(acts.editDowntime),
      debug('DowntimesEffects::editDowntime'),
      tap((payload) => this.downtimeRepo.setDowntimeDTO(payload.downtime))
    )
  );

  updateDowntime$ = createEffect(
    (actions) =>
      actions.pipe(
        ofType(acts.updateDowntime),
        debug('DowntimesEffects::updateDowntime'),
        switchMap((payload: { downtime: UpdateDowntineDTO }) =>
          this.oDataService.updateDowntime(payload.downtime)
        ),
        map((res) => {
          return res.d.outputCode === 0
            ? messagesActs.showMessage({
                category: SUCCESS,
                messageOrError: 'Downtime updated successfuly!',
                showAs: NOTIFICATION,
              })
            : messagesActs.showMessage({
                category: ERROR,
                messageOrError:
                  applicationErrors[ApplicationErrorCode.CannotUpdateDowntime],
                showAs: NOTIFICATION,
              });
        })
      ),
    { dispatch: true }
  );

  deleteDowntime$ = createEffect(
    (actions) =>
      actions.pipe(
        ofType(acts.deleteDowntime),
        debug('DowntimesEffects::deleteDowntime'),
        switchMap((payload: { downtime: DeleteDowntineDTO }) =>
          this.oDataService.deleteDowntime(payload.downtime)
        ),
        map((res) => {
          return res.d.outputCode === 0
            ? messagesActs.showMessage({
                category: SUCCESS,
                messageOrError: 'Downtime deleted successfuly!',
                showAs: NOTIFICATION,
              })
            : messagesActs.showMessage({
                category: ERROR,
                messageOrError:
                  applicationErrors[ApplicationErrorCode.CannotDeleteDowntime],
                showAs: NOTIFICATION,
              });
        })
      ),
    { dispatch: true }
  );
}
import { Injectable, InjectionToken } from '@angular/core';
import {
  createStore,
  distinctUntilArrayItemChanged,
  propsFactory,
} from '@ngneat/elf';
import {
  selectAllEntities,
  setEntities,
  withEntities,
} from '@ngneat/elf-entities';
import { localStorageStrategy, persistState } from '@ngneat/elf-persist-state';
import { distinct, distinctUntilChanged } from 'rxjs';
import { Downtime } from 'src/app/core/domain/models/downtime';
import { DowntimesFilter } from './downtimes.state';
import { NodeDetails } from './models/node-details';
import { DowntimeDTOS } from './types';

const { withFilter, selectFilter, setFilter } = propsFactory('filter', {
  initialValue: DowntimesFilter.ALL_DOWNTIMES,
});

const { withLoading, setLoading, selectLoading } = propsFactory('loading', {
  initialValue: false,
});

const { withDowntimeDto, setDowntimeDto, selectDowntimeDto, resetDowntimeDto } =
  propsFactory('downtimeDto', {
    initialValue: {} as DowntimeDTOS,
  });

const {
  withWorkUnitForDowntime,
  setWorkUnitForDowntime,
  selectWorkUnitForDowntime,
} = propsFactory('workUnitForDowntime', {
  initialValue: [] as NodeDetails[],
});

const store = createStore(
  {
    name: 'downtimes',
  },
  withEntities<Downtime, 'downId'>({ idKey: 'downId' }),
  withWorkUnitForDowntime(),
  withFilter(DowntimesFilter.ALL_DOWNTIMES),
  withLoading(false),
  withDowntimeDto(undefined)
);

export const persist = persistState(store, {
  key: 'downtimes',
  storage: localStorageStrategy,
});

export const DOWNTIMES_REPOSITORY = new InjectionToken<DowntimeRepository>(
  'DOWNTIMES_REPOSITORY'
);

@Injectable({ providedIn: 'root' })
export class DowntimeRepository {
  filter$ = store.pipe(selectFilter()).pipe(distinctUntilChanged());
  downtimes$ = store.pipe(selectAllEntities());

  loading$ = store.pipe(selectLoading()).pipe(distinctUntilChanged());
  workUnitsForDowntime$ = store
    .pipe(selectWorkUnitForDowntime())
    .pipe(distinctUntilArrayItemChanged());

  downtimeDto$ = store.pipe(selectDowntimeDto());

  constructor() {}

  setDowntimes(downtimes: Array<Downtime>) {
    store.update(setEntities(downtimes));
  }

  setDowntimeDTO(downtime: DowntimeDTOS) {
    store.update(setDowntimeDto(downtime));
  }

  setWorkUnitForDowntime(workunits: NodeDetails[]) {
    store.update(setWorkUnitForDowntime(workunits));
  }

  setLoading(loading: boolean) {
    store.update(setLoading(loading));
  }

  reset() {
    store.update(resetDowntimeDto());
  }
}

core.module.ts

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,
    SharedModule,
    EffectsNgModule.forFeature([
      DowntimesEffects,
    ]),
  ]
})
export class CoreModule {}

core.module.ts

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,
    SharedModule,
    EffectsNgModule.forRoot([]),
  ]
})
export class AppModule {}
override ngOnInit(): void {
      this.downtimeRepository.downtimeDto$
        .pipe(takeUntil(this.onDestroy$))
        .subscribe({
          next: (downtime) => {
            // OPen a dialog (x2)
            this.addOrEditDowntimeService.createOrEditDowntime(downtime);
          },
        });
    }

I can't reproduce this behaviour with a clean slate project.

If someone have hint let me know.

PS:

OS: Mac OS 12 NPM packages:

"@ngneat/effects-ng": "^2.0.0",
"@ngneat/elf": "^1.5.6",
"@ngneat/elf-cli-ng": "^1.0.0",
"@ngneat/elf-devtools": "^1.2.1",
"@ngneat/elf-entities": "^4.3.0",
"@ngneat/elf-pagination": "^1.0.0",
"@ngneat/elf-persist-state": "^1.1.1",
"@ngneat/elf-requests": "^1.1.2",
guillaumemaka commented 2 years ago

I solves my issue, my core module was imported by a lazy loaded module, so the effects were registered twice,