Search code examples
angularngrx

Listen to ngrx Actions from Component


I'm building an app using ngrx/angular8, and there is one scenario where i want to respond to an action from different components. The traditional way is to add another property to the store and create the reducer/selector for it. Problem is I want for the other components to respond to the event even if it has the same value. e.g. lets break down:

  1. I clicked on a button in one of the components, it dispatches the action this.store.dispatch( LayoutActions.scrollToSession({id: session_id})
  2. I want 3 different component to respond to this action in some way, even if the session_id is the same. So If i created a property in the reducer, the selector will get the update only for the first time and won't fire for subsequent actions.

My solution was simply to dispatch the action, and in the component im listening for the action:

        this.actions$.pipe(
            ofType(LayoutActions.scrollToSession),
            takeUntil(this.unsubscribe)
        ).subscribe(id => { 

            if ((!id.id) || (!this.messages_list)) return;
            this.messages_list.scrollToElement(`#session-${id.id}`, {left: null, top: null});

        });

My question is, is this the right approach? listening for the actions in the components directly and what are my alternatives if any? I though of adding a random prefix when dispatch the action, to change the store state and remove it later in the selector, but that doesn't feel right.


Solution

  • UPDATED

    The right way is to always rely on the store state, not on its actions.

    possible solution

    store.ts

    import {Injectable} from '@angular/core';
    import {Actions, createEffect, ofType} from '@ngrx/effects';
    import {Action, createAction, createFeatureSelector, createReducer, createSelector, on, props} from '@ngrx/store';
    import {delay, map} from 'rxjs/operators';
    
    // actions
    export const setScroll = createAction('scroll', props<{id?: string, shaker?: number}>());
    export const causeTask = createAction('task', props<{scrollId: string}>());
    
    // reducer
    export interface State {
        scroll?: {
            id: string,
            shaker: number,
        };
    }
    
    const reducer = createReducer(
        {},
    
        on(setScroll, (state, {id, shaker}) => ({
            ...state,
            scroll: id ? {id, shaker} : undefined,
        })),
    );
    
    export function coreReducer(state: State, action: Action): State {
        return reducer(state, action);
    }
    
    export const selectState = createFeatureSelector<State>('core');
    
    export const selectFlag = createSelector(
        selectState,
        state => state.scroll,
    );
    
    // effects
    @Injectable()
    export class Effects  {
        public readonly effect$ = createEffect(() => this.actions$.pipe(
            ofType(causeTask),
            delay(5000),
            map(({scrollId}) => setScroll({id: scrollId, shaker: Math.random()})),
        ));
    
        constructor(protected readonly actions$: Actions) {}
    }
    

    app.component.ts

    import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
    import {Store} from '@ngrx/store';
    import {filter, map} from 'rxjs/operators';
    import {causeTask, selectFlag, setScroll} from 'src/app/store';
    
    @Component({
        selector: 'app-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.scss'],
        changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppComponent implements OnInit {
    
        constructor(protected store: Store) {
        }
    
        public ngOnInit(): void {
            // reset of the scrolling state
            this.store.dispatch(setScroll({}));
    
            this.store.select(selectFlag).pipe(
                filter(f => !!f),
                map(f => f.id),
            ).subscribe(value => {
                this.store.dispatch(setScroll({})); // reset
                alert(value); // <- here you should use the scrolling.
            });
    
            // some long task which result should cause scrolling to id.id.
            this.store.dispatch(causeTask({scrollId: 'value of id.id'}));
            this.store.dispatch(causeTask({scrollId: 'value of id.id'}));
        }
    }
    

    app.module.ts

    import {NgModule} from '@angular/core';
    import {BrowserModule} from '@angular/platform-browser';
    import {EffectsModule} from '@ngrx/effects';
    import {StoreModule} from '@ngrx/store';
    import {coreReducer, Effects} from 'src/app/store';
    
    import {AppComponent} from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
          StoreModule.forRoot({
            core: coreReducer,
          }),
          EffectsModule.forRoot([
            Effects,
          ]),
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    ORIGINAL

    If you need the actions you can use their stream.

    import {StoreActions, StoreState} from '@core/store';
    
    
    ...
        constructor(
            protected readonly storeActions: StoreActions,
        ) {}
    ...
    
    ...
            // listening on success action of company change request.
            this.storeActions
                .ofType(CompanyProfileActions.UpdateBaseSuccess)
                .pipe(takeUntil(this.destroy$))
                .subscribe();
    ...