Yehia A.Salam
Yehia A.Salam

Reputation: 2028

Ngrx Effects Calling API and Comparing with Store

So I've been trying to figure this out for 2 days and I gave up, rxjs is very tricky specially when it comes to the map family.

I have an action MessageRequested with an id as the paramter, that should initiate an api call to get the Message, and then check whether the corresponding visitor is in the store or not. If it's not in the store it will dispatch two actions, MessageLoaded and VisitorRequested. If it's in the store, we will just dispatch one action, MessageLoaded. So logic would be in that order:

  1. MessageRequested was dispatched with an id as the input
  2. Now we call the api with id as the parameter, we have a Message object returned. Message definition below. It has a visitor_id property: mergeMap((action) => this.converseService.getMessage( action[0].message_id ))
  3. Now we query the store, whether the visitor_id in the Message object exists in the Visitor state (this.store.pipe(select(selectVisitorById( m.visitor_id ) ))
  4. Now we have an if condition we need to apply,

    a. if select returned something, then the visitor is already in the store, and we will return just the MessageLoaded action.

    b. If select didnt return a visitor, we now have to dispatch an additional action VisitorRequested to request the visitor. We still need to dispatch the MessageLoaded action. So thats 2 actions.

Now to the code:

    loadMessage$ = this.actions$
        .pipe(
            ofType<MessageRequested>(MessageActionTypes.MessageRequested),
            mergeMap((action) => this.converseService.getMessage( action[0].message_id )),
            map(m => m.model),
            withLatestFrom(  this.store.pipe(select(selectVisitorById( m.visitor_id ) ))),
            tap( ([message,visitor]) => {
                if (visitor == null) new VisitorRequested(message.visitor_id);
            }),
            map( ([message, visitor]) => {
                return new MessageLoaded({message});
            } )
        );
export class Message {
    id: number;
    message: string;
    visitor_id: number;
    is_from_visitor: boolean;
    created_at: string;
}

export class MessageResponse extends Response {
  model: Message
}

export class Response{
  message:String;
  didError: boolean;
  errorMessage: string;
}

Code looks alright, except that withLatestFrom does not accept inputs, so I can't pass the m.visitor_id variable, and now I have to think of another approach. I tried using forkJoin and concatMap, but didnt make it.

What would be right operators to utilize in the scenario?

Update #1

This is what finally worked with the help of all the answers:

    @Effect()
    loadMessage$ = this.actions$
        .pipe(
            ofType<MessageRequested>(MessageActionTypes.MessageRequested),
            mergeMap((action) => this.converseService.getMessage( action.message_id )),
            map(m => m.model),
            concatMap(message =>
                of(message).pipe(
                  withLatestFrom(this.store.pipe(select(selectVisitorById(message.visitor_id))))
                )
            ),            
            tap( ([message,visitor]) => {
                if (!(visitor)) this.store.dispatch(new VisitorRequested(message.visitor_id));
            }),
            map( ([message, visitor]) => {
                return new MessageLoaded({message});
            } )
        );

Upvotes: 1

Views: 1351

Answers (2)

Friso Hoekstra
Friso Hoekstra

Reputation: 885

If you want to pass down some data from the store along with data from an action, you can pass a map function to withLatestFrom():

someEffect$ = createEffect(() =>
        this.actions$.pipe(
            ofType(SOME_ACTION),
            withLatestFrom(this.store.select(yourFeature), (action, data) => ({ dataFromStore: data, payload: action.payload })),
            switchMap(props => {
                // do something fun
            })  
        )
    );

As stated in https://ngrx.io/guide/effects, it is also recommended to wrap withLatestFrom in a flattening operator, because withLatestFrom will listen through the select() right away regardless of the action coming in.

concatMap(action => of(action).pipe(
     withLatestFrom(this.store.pipe(select(yourFeature)))
))

Upvotes: 0

timdeschryver
timdeschryver

Reputation: 15505

tap does not return a value, it's just a void. Instead, return an array of actions depending on the condition:

 switchMap(([message,visitor]) => {
    if (visitor == null) return [new VisitorRequested(message.visitor_id), new MessageLoaded({message})];
    return [new MessageLoaded({message})]
 })

Imho, the more correct answer would be to have 2 effects:

1) listen to MessageActionTypes.MessageRequested and dispatch MessageLoaded 2) listen to MessageActionTypes.MessageRequested and dispatch VisitorRequested if it isn't in the store. Or even, put the logic of VisitorRequested in this effect.

Upvotes: 1

Related Questions