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
What I would need is instead something like below.
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?
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))
);
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.