Search code examples
angularrxjsngrxngrx-effects

Dispatch action N times but only make one API call in effect and combine result N times


I have multiple instances of a the same component in an Angular application. Each instance of the component will dispatch an action [Action] LoadData to load data from an external API. The payload of the action has information about which component who dispatched the action, to be able to store it in the correct corresponding place in the store.

The fetched data will be the same each time, no matter what other payload is included in the action. Since this is the case, there's no need for me to do multiple API calls to load the same data, so I can cancel out any previous unfinished calls with switchMap.

For this I have two effects, one to load the data:

@Effect({ dispatch: false })
loadData$ = this.actions$.pipe(
    ofType<LoadData>(LOAD_DATA),
    switchMap((action) => { // This will cancel out any previous service call
        return this.service.loadData().pipe(
            tap(val => {
                console.log(`Loading data...`)
                this.dataSubject$.next(val) // When the data is fetched, emit on the subject
            })
        );
    })
);

and one to handle the dispatched action combined with the data

@Effect()
handleActionAndDataLoaded$ = combineLatest(
    this.actions$.pipe(
        ofType<LoadData>(LOAD_DATA)
    ),
    this.dataSubject$.asObservable()
).pipe(
    tap(_ => console.log(`All done`))
)

and where dataSubject$ is a Subject.

This is where the tricky part for me comes in. I need the second effect to be triggered n times if the LoadData action has been dispatched n times and then handle the data in combination with other action payloads accordingly. What I now instead get is one trigger of the second effect.

Reading into combineLatest and looking at marble diagrams, this is of course the expected behaviour.

Represented in marble diagrams I get something like

enter image description here

What I would need is instead something like below.

enter image description here

I know there are other ways of solving the whole issue by changing other things in the architecture of the application, but this is what I have to work with at the moment, and it seems to me like an interesting rxjs question!

How can this be achieved? What combination of operators am I missing?


Solution

  • I'm assuming it should look more similar to the first diagram:

    actions -1-2-3-----------
    data    ---------B-------
    result  ---------(1B2B3B)
    

    Otherwise it looks like you expect LoadData actions to be paired with previously fetched data.

    You can create a state machine using merge and scan:

    const createEffectWithScan = (actions$: Observable<string>, subject$: Observable<string>) =>
      merge(
        actions$.pipe(map(x => ({ type: 'action', value: x }))),
        subject$.pipe(map(x => ({ type: 'data', value: x }))),
      ).pipe(
        scan(
          (a, b) => b.type === 'action'
            ? { data: null, toEmit: [], buffer: [...a.buffer, b.value] }
            : { data: b.value, toEmit: a.buffer, buffer: []  }
          , { data: null, buffer: [], toEmit: [] }
        ),
        filter(x => x.data !== null),
        concatMap(x => x.toEmit.map(a => a + x.data))
      );
    

    Stackblitz

    As you've mentioned, this is a nice exercise, but should probably be solved another way. Poking values imperatively via subjects is not a good solution.