Home > Software engineering >  Generate Observable Array using the original Array itself
Generate Observable Array using the original Array itself

Time:03-06

I have a service (MessageService) that emits some messages based on some other app events.
Then, I have a component MessagesComponent and I'd like to render all the messages that were emitted by my Service. My approach is to use an Observable<Message[]> to store and reactively update the UI every time a new Message arrives. Here's my working solution

@Component({
  selector: 'app-messages',
  template: `
    <h2>Received messages</h2>
    <ol>
      <li  *ngFor="let message of messages$ | async">
        {{ message }}
      </li>
    </ol>
  `,
  styles: [``]
})
export class MessagesComponent implements OnInit, OnDestroy {
  destroy$ = new Subject<void>();
  messages$ = new BehaviorSubject<Message[]>([]);
  
  constructor(private messagesService: MessagesService) {}

  ngOnInit(): void {
    this.messagesService.message$
      .pipe(
        takeUntil(this.destroy$),
        switchMap((message: Message) =>
          this.messages$.pipe(
            take(1),
            map((all: Message[]) => {
              return [...all, message];
            })
          )
        )
      )
      .subscribe((messages: Message[]) => {
        this.messages$.next(messages);
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Right now, I start with an empty BehaviorSubject array to store all the messages. Then, in the ngOnInit hook, I subscribe to the MessagesService's message$ and after a new emission I am combining what I have in the BehaviorSubject with the new message thus emitting a new array with all the items. My solution seems to work, but I'd like to avoid the subscription in NgOnit (if possible). I was thinking in something like this

messages$: Observable<Message[]> = this.messagesService.message$.pipe(
        switchMap((message: Message) =>
          this.messages$.pipe(
            startWith([]),
            take(1),
            map((all: Message[]) => [...all, message])
          )
        )
      );

I'm trying to use the array itself as part of their own initialization using SwitchMap StartWith however it doesn't seems to work, so I'm looking for some other ideas :)

Oh, the Message type is just a simple interface with some properties.

export interface Message {
  id: string;
  value: string;
}

CodePudding user response:

You can use the scan operator to accumulate state. It will have access to the previous emission and you can build upon that.

export class AppComponent {
  
  messages$ = this.messagesService.message$.pipe(
    scan((all, message) => all.concat(message), [])
  );

  constructor(private messagesService: MessagesService) { }

}

When scan receives an emission from mesageService.message$, it will run the function

(all, message) => all.concat(message)

and emit the result. message is the value received from the service. The first time, all has a value of our initial seed value ([]). After that, it is the previously emitted value;

Notice the component code is greatly simplified; you don't need: Subject, BehaviorSubject, subscription, ngOnInit, ngOnDestroy, takeUntil, switchMap, nor take :-)

Here's a little StackBlitz demo.

  • Related