1

How can an RxJS Observable emit a new value, if the underlying event stream doesn't emit a new value?

Given an Angular component which uses an async pipe to display a user's name;

export class MyComponent implements OnInit {

  public user$!: Observable<User>;

  constructor(private readonly ms: MyService)

  ngOnInit() {
    this.user$ = this.ms.getUser();
  }
}
<ng-container *ngIf="user$ | async as user; else empty">
  {{ user.name }}
</ng-container>

<ng-template #empty>
  No user here.
</ng-template>

...imagine some time passes and I want to try getting the user again by calling the asynchronous function this.ms.getUser(), how can I do this?

AFAIK, I cannot simply overwrite the Observable $captcha;

export class MyComponent implements OnInit {

  public user$!: Observable<User>;

  constructor(private readonly ms: MyService)

  ngOnInit() {
    this.user$ = this.ms.getUser();
  }

  getUserAgain() {
    // Wrong. This would be a new/different Observable.
    // The UI would not update.
    this.user$ = this.ms.getUser();
  }
}

The service MyService returns an Observable because it's just a thin wrapper around Angular's HttpClient.

Jack
  • 10,313
  • 15
  • 75
  • 118
  • This may be useful: https://stackoverflow.com/questions/44911251/how-to-create-an-rxjs-retrywhen-with-delay-and-limit-on-tries – Tomer Almog Jan 22 '20 at 08:01

2 Answers2

3

You could also use repeatWhen:

export class MyComponent implements OnInit {

  public user$!: Observable<User>;
  private repeat$ = new Subject()

  constructor(private readonly ms: MyService) {}

  ngOnInit() {
    this.user$ = this.ms.getUser().pipe(
      repeatWhen(() => repeat$)
    );
  }

  getUserAgain() {
    this.repeat$.next();
  }
}

https://stackblitz.com/edit/eoznkm

frido
  • 13,065
  • 5
  • 42
  • 56
  • 1
    I really love this answer!!! I was used doing this the way as posted in the answer before and always forgot the pattern. Using `repeatWhen()` is so much more descriptive and intuitiv. Also it reads better in the pipe. This should be the accepted answer. – Florian Leitgeb Jan 23 '20 at 09:49
1

I'd recommend to use observables as much as possible and stay in a "reactive mind":

export class MyComponent {
  public refreshUser$: Subject<void> = new Subject();

  public user$!: Observable<User> = this.refreshUser$.pipe(
    startWith(undefined),
    switchMap(() => this.ms.getUser())
  );

  constructor(private readonly ms: MyService) {}

  public getUserAgain() {
    this.refreshUser$.next();
  }
}
maxime1992
  • 22,502
  • 10
  • 80
  • 121
  • thanks. Can you explain the need for `startsWith`? AFACT, the async pipe will subscribe when the component is rendered and `switchMap` called immediately. – Jack Jan 22 '20 at 02:40
  • Are you using `startWith` to force the Observable to emit a value when the component is rendered? Without `startWith` no values would be emitted until `getUserAgain` is called? – Jack Jan 22 '20 at 03:24
  • 1
    Exactly. This saves you to call getUserAgain from ngOnInit :) – maxime1992 Jan 22 '20 at 09:34