cmatthews.dickson
cmatthews.dickson

Reputation: 697

How to have an effect wait on an Observable with @ngrx/effects

I am using @ngrx/effects in my Angular 2 application and I'm having some issues trying to implement an effect that is dependent on a condition based on the state of the application.

In my state, I have a map of Location -> Capabilities, keyed by locationId (as well as other maps of Location -> *). When ACTION_A occurs, I only want it proceed once there are capabilities that are up-to-date for the current location. When ACTION_A is dispatched, the capabilities may already be up-to-date, however, the application could also be in the process of loading new ones.

With the code below, once ACTION_A is dispatched once, isOutOfDate is called every time any action is dispatched to the store. So, it seems like the observable is still active, even after the up-to-date filter has been met. However, if getCapabilities is replaced with getCapabilitiesWorking, then isOutOfDate is only called until the filter condition is met each time the ACTION_A action is dispatched, as I would expect from either implementation.

So, it seems like I am missing something with how combineLatest or filter or take works ...

or perhaps I am approaching this all wrong ...

I would rather be able to use the common function for my 'current location' selectors

Any ideas?

@Effect() route$ = actions$
    .ofType(ACTION_A)
    .switchMap(() => this._store.let(getCapabilities)
        .filter(capabilities => !isOutOfDate(capabilities))
        .take(1)
    )
    .map( do some stuff );

// Selector function that causes excess observable emits
export const getCapabilities =
     compose(fromLocations.currentLocationLookup(fromLocations.getCapabilities), fromRoot.getLocations);

// Selector function works as expected
export const getCapabilitiesWorking = function (state$: Observable<AppState>): Observable<Capability> {
    return state$
        .let(fromRoot.getLocations)
        .map(l => l.capabilitiesByLocation[l.currentId]);
};

// fromLocations
// -------------

export function getCapabilities(state$: Observable<LocationsState>): Observable<{ [id: string]: Capability }> {
    return state$.map(s => s.capabilitiesByLocation);
}

// Find the value in 'collection' corresponding to the current location
export function currentLocationLookup<T>(
    collection: (state$: Observable<LocationsState>) => Observable<{ [id: string]: T }>
): (state$: Observable<LocationsState>) => Observable<T> {
    return (state$: Observable<LocationsState>): Observable<T> => {
        return Observable.combineLatest(
            state$.let(getCurrentLocation),
            state$.let(collection),
            (locId, map) => map[locId]
        );
    };
}

EDIT: To clarify, the isOutOfDate function keeps getting invoked, however the "do some stuff" does not...

Upvotes: 5

Views: 11532

Answers (3)

lPotatoes
lPotatoes

Reputation: 129

withLatestFrom won't work, since resulted observable will emit the output only when source emits(action dispatched), in accordance with doc you need combineLatest, so:

@Effect() route$ = actions$
  .ofType(ACTION_A)
  .combineLatest(this._store.let(getCapabilities))
  .filter(([action, capabilities]) =>!isOutOfDate(capabilities)) 
  .map( do some stuff );

Upvotes: 6

tonobody
tonobody

Reputation: 81

You might try to replace the filter in the switchMap with skipWhile

So the Effect will be:

@Effect() route$ = actions$
.ofType(ACTION_A)
.switchMap(() => this._store.let(getCapabilities)
    .skipWhile(capabilities => !isOutOfDate(capabilities))
    .take(1)
)
.map( do some stuff );

This helped me in a similar use case :)

Upvotes: 8

user1614543
user1614543

Reputation: 111

Try using withLatestFrom method. Your effect can look like

@Effect() route$ = actions$
.ofType(ACTION_A)
.withLatestFrom(this._store.let(getCapabilities))
.filter(([action, capabilities]) =>!isOutOfDate(capabilities)) 
.map( do some stuff );

Upvotes: -1

Related Questions