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.