Observable updates UI only once

267 Views Asked by At

I am fairly new to Angular (10) and try to grasp the concept of Observables. I have a Service and a Component, the services fills an array with participants and the component is supposed to display them

Service

export class MercureService {

  private _participants = new BehaviorSubject<ParticipantDTO[]>([]);
  private participantList: Array<ParticipantDTO> = [];

  constructor() {
  }

  get participants$(): Observable<Array<ParticipantDTO>> {
    return this._participants.asObservable();
  }

  admin_topic(topic: AdminTopic): void {

    const url = new URL(environment.mercure);
    url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);

    const eventSource = new EventSource(url.toString());
    eventSource.onmessage = event => {
      const json = JSON.parse(event.data);
      console.log('NEW EVENT');
      // ...
      if (json.event === BackendEvents.NEW_PARTICIPANT) {
        this.participantList.push({voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0'});
        this._participants.next(this.participantList);
      }
    };
  }

Component.ts

export class StartSessionComponent implements OnInit, OnDestroy {
  // ...
  unsubscribe$: Subject<void> = new Subject<void>();
  participantList: ParticipantDTO[];

  constructor(
    // ...
    public mercure: MercureService
  ) {}

  ngOnInit(): void {
    this.mercure.participants$
      .pipe(takeUntil(this.unsubscribe$))
      .subscribe((data) => {
        this.participantList = data;
      });

    this.mercure.admin_topic({sessionUuid: '123', userUuid: '456'});
  }

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

Component.html

...
  <div *ngFor="let participant of this.mercure.participants$ | async">
    <p>{{participant.voter}} - Vote ({{participant.vote}})</p>
  </div>
...

So I am no sending a message and it gets picked up by the EventSource, the console says

NEW EVENT

and the UI gets updated (adds a new <p>WHATEVER NAME - VOTE XXX</p>). However, when I send a second message from the Server, I get

NEW EVENT

again, but the UI does not get updated. I suspect I am doing sth wrong with the Observable, can somebaody help please?

2

There are 2 best solutions below

0
On BEST ANSWER

The issue is that EventSource events are emitted outside of Angular, so that whatever happens inside your eventSource.onmessage is not updating the UI properly. That's why you need to wrap whatever is happening inside the onmessage to be run inside the Angular with the help of NgZone.

See example:

  constructor(
    private zone: NgZone
  ) { }

  admin_topic(topic: AdminTopic): void {
    const url = new URL(environment.mercure);
    url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);

    const eventSource = new EventSource(url.toString());
    eventSource.onmessage = event => {
      this.zone.run(() => { // This is what you need
        const json = JSON.parse(event.data);
        console.log('NEW EVENT');
        // ...
        if (json.event === BackendEvents.NEW_PARTICIPANT) {
          this.participantList.push({ voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0' });
          this._participants.next(this.participantList);
        }
      })
    };
  }

Another solution would be to use Event Source wrapper that will do the exact same thing for you and will give you the ease of use. It will be also wrapped in the Observable, so that you can have rich experience. See this article for example: Event Source wrapped with NgZone and Observable

0
On

this is an expected behavior since this.participantList refers to a value which is already stored by the subject (since the reference hasn't been changed), you might want to spread your array to create a new array each time you want to update its value:

this._participants.next(....this.participantList);