Build Angular Store with Rxjs

482 Views Asked by At

I have this Angular app in which I want to create a custom store for chat objects.

(codesandbox: https://codesandbox.io/s/chat-store-yw4yz)

Note: I know there are tools like ngrx but this is part of a bigger project and I would prefer to use this custom implementation so I understand better the behaviour of the store.

Context

The idea to make it quick and efficient is to have into the store two types of records:

  • A dictionary (as a Map) with key = chat id and value = chat object
  • An index (as a Map) for grouping chats by groupId, where key = groupId and value = chatId

So I have this interface definition:

export interface ChatStoreModel {
  chats: Map<number, Chat>;
  chatsByGroup: Map<number, number[]>;
}

When a chat object is saved into the Store two things should happen:

  • chat object is saved into store chats
  • chat id is saved into store chatsByGroup in the corresponding groupId

Problem

It is not working to get the chat objects from a group. I do the following:

  • get chat ids list from store chatsByGroup using groupId
  • for each chat id, get element from store chats

Code:

getGroupChats$(groupId: number): Observable<Chat[]> {
    return this._store.select("chatsByGroup").pipe(
      map((chatsByGroup) => chatsByGroup.get(groupId) || []),
      switchMap((ids) => {
        if (!ids || !ids.length) {
          return of([]);
        }
        const chatsList$: Observable<Chat>[] = ids.map((id) =>
          this._store.select("chats").pipe(map((chats) => chats.get(id)))
        );
        return forkJoin([...chatsList$]).pipe(
          map((list) => {
            return ([] as Chat[]).concat(...list);
          })
        );
      }),
      shareReplay(1)
    );
  }

Debugging this code, it reaches the line with return forkJoin and ids have the list of chats but it never reaches the line map((list) => { so the app does not display the chat list.

Any help will be appreciated, thanks!

1

There are 1 best solutions below

2
On BEST ANSWER

whenever any of that observables completes without emitting any value, forkJoin will complete at that moment as well and it will not emit anything either

This is from the RxJS ForkJoin Doc. My guess is that the one of the Observable that you pass to the forkJoin completes without emitting anything, thus forkJoin instantly completes without emitting anything.

Try using defaultIfEmpty operator to be sure that this situation doesn't happen. I suggest doing something like so:

const chatsList$: Observable<Chat>[] = ids.map((id) =>
  this._store.select("chats").pipe(map((chats) => chats.get(id))),
  defaultIfEmpty(null),
);

forkJoin will wait for all passed observables to complete

This is again from the docs. This means that all of the Observables that you pass to the forkJoin have to complete in order for the forkJoin to emit value itself.

My guess is that this._store.select("chats").pipe(map((chats) => chats.get(id))), this line will run endlessly so you need to provide take(1) for it to be completed, otherwise forkJoin won't emit anything. So try something like this:

const chatsList$: Observable<Chat>[] = ids.map((id) =>
  this._store.select("chats").pipe(map((chats) => chats.get(id))),
  take(1),
  defaultIfEmpty(null),
);

Lastly if you don't want to use take(1), by which I mean you want to be subscribed to the Observable as long as it is emitting values, you might want to use combineLatest instead of forkJoin. That's because forkJoin waits for the completion to emit something, while combineLatest will emit the latest values of the provided Observables.