How to use withLatestFrom with a selector in ngrx?

10,913

Solution 1

This is covered in the NgRx Docs - Incorporating State but I found Brandon Roberts github comment more useful:


There are two predominant things you do when listening to an action after using the ofType operator:

actions.pipe(
  ofType('SOME_ACTION')
  someMap(action => doSomething(action))
)

and

actions.pipe(
  ofType('SOME_ACTION'),
  withLatestFrom(store.select(someThing)),
  someMap(([action, latest]) => doSomething(action, latest))
)

These have two different behaviors as far as effects are concerned. The first one doesn't do anything until the action is dispatched, which is what it should do.

The second one immediately subscribes to the withLatestFrom whether the action is dispatched yet or not. If that state used by the selector is not initialized yet, you could get an error you're not expecting. In order to make it "lazy" you end up having to nest it within a flattening observable operator.

actions.pipe(
  ofType('SOME_ACTION'),
  someMap(action =>
    of(action).pipe(
      withLatestFrom(store.select(someThing)),
      someMap(([action, latest]) => doSomething(action, latest))
    )
)

Applying this gives the following:

  getDoc$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(fromDocs.getDocument),
      switchMap(action =>
        of(action).pipe(
          withLatestFrom(
            this.store.select(fromDocs.getById, { id: action.payload.id })
          ),
          map(([action, latest]) => {
            return fromDocs.someAction();
          })
        )
      )
    );
  });

Stackblitz

Solution 2

Use concatLatestFrom from Ngrx (lazy version of withLatestFrom) to mitigate the situation where the original state has not been initiated before needed.

import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';

concatLatestFrom(() => this.store.select(fromDocs.getById, { id: action.payload.id }),
      map(([action, latest]) => {
        return fromDocs.someAction();
      })
Share:
10,913
Jon Sud
Author by

Jon Sud

Updated on June 05, 2022

Comments

  • Jon Sud
    Jon Sud almost 2 years

    I have ngrx application, and I can't get the value from a selector in the effect, by the payload I sent in the component.

    I write an example code of what I deal with:

    This is my state, a simple docs array in it.

    export const initialState = {
      docs: [{ id: 1, name: "doc1" }, { id: 2, name: "doc2" }]
    };
    
    export const reducer = createReducer(
      initialState,
    );
    

    I dont handle any action for this questions, this is not the issue.

    In my component I get the array using a selector:

    docs$ = this.store.pipe(select(fromStore.getAllDocs));
    
    {{docs$ | async | json}}
    

    the reducer:

    export const selectDocsState = createFeatureSelector("docs");
    
    export const selectDocsStatusState = createSelector(
      selectDocsState,
      state => state["docs"]
    );
    
    export const getAllDocs = createSelector(
      selectDocsStatusState,
      getDocs
    )
    

    I also have a selector to get by id:

    {{docsById$ | async | json }}
    
    docsById$ = this.store.pipe(select(fromStore.getById, { id: 1 }));
    

    in the reducer:

    export const getById = createSelector(
      selectDocsStatusState,
      getDocs,
      (state, docs, props) => {
        console.log({ props });
        return docs.docs.docs.filter(d => d.id === props.id);
      }
    )
    

    So far everything good. I get the docs array and display it.

    Now, in my component I dispatch some action and catch by effect:

    this.store.dispatch({ type: 'GET_DOC', payload: { id: 2 } });
    

    And in the effect I want to know if this id is in the store or not.

    So I use withLatestFrom from rxjs with the getById selector.

    withLatestFrom((a: any) =>
      this.store.pipe(select(fromDocs.getById, { id: a.payload.id }))
    ),
    

    The problem is i'm not getting the value from the selector instend I get the store instance.

    tap(v => {
      console.log({ v });
    }),
    

    The full effect:

    getDoc$ = createEffect(() =>
        this.actions$.pipe(
          ofType("GET_DOC"),
          map(a => a),
          tap(a => {
            console.log({ a });
          }),
          withLatestFrom((a: any) =>
            this.store.pipe(select(fromDocs.getById, { id: a.payload.id }))
          ),
          tap(v => {
            console.log({ v }); // <--------------- HERE SHOULD BE THE DOC AND THE ACTION.PAYLOAD, v[0].a.payload, v[1].id
          }),
          exhaustMap(payload => {
            return of({ type: "SOME_ACTION", payload: null });
          })
        )
      );
    

    Here is my full example in stackbliz.

    What do I missing here?