Search code examples
rxjsangular6es6-promiseangularfire2rxjs6

RXJS 6 - toPromise


Im currently trying to convert an Observable to a Promise. But when I call that method nothing happens. Im using Angular 6.

Service

  public create(form: StoryForm): Promise<void | string> {
    const key: string = this.afStore.createId();

    return this.auth.authState.pipe(map(res =>
      <Story>{
        title: form.title, content: form.content, createdAt: new Date(), sid: key,
        uid: res.uid, username: res.displayName
      }
    )).toPromise().then((story: Story) =>
      this.afStore.doc(`stories/${key}`).set(story).catch(err => err.message));
  }

Component

  public save() {
    this.triedToSave = true;
    if (this.storyForm.valid) {
      this.storyService.create(this.storyForm.value)
        .then(() => this.router.navigate(['/stories']))
        .catch((err: string) => this.notify.danger(err));
    }
  }

What save should do is to navigate or at least displaying the error.

Auth

How authstate is implemented: It returns an observable of some user information. It is implemented in a different service and looks like this:

  public get authState(): Observable<firebase.User> {
    return this.afAuth.authState;
  }

Edit

What confuses me is that, if I use a mock Object than it suddenly works:

  public create(form: StoryForm) {
    const key: string = this.afStore.createId();

    return of({uid: 'blubb', displayName: 'kdsjf', photoUrl: 'kjdfkjfd'}).pipe(map(user => {
      return {
        title: form.title, content: form.content, createdAt: new Date(), sid: key,
        uid: user.uid, username: user.displayName, photoUrl: user.photoURL
      } as Story;
    })).toPromise();
  }

But I wonder why toPromise does not work on the example above...


Solution

  • Im guessing that nothing happens because when you trigger the save method, nothing comes out of the authState. Apparently you expect that the authState observable or Subject will always trigger some output, which is only the case in specific cases.

    The code below creates a -new- observable that listens to authState.

    return this.auth.authState.pipe(map(res =>
          <Story>{
            title: form.title, content: form.content, createdAt: new Date(), sid: key,
            uid: res.uid, username: res.displayName
          }
        )).toPromise().then((story: Story) =>
          this.afStore.doc(`stories/${key}`).set(story).catch(err => err.message));
    

    This code is only triggered by the save method though. My guess is that authState is either an observable, or a subject. You code would only work when the authState is passed a new value -after- the save method is triggered.

    Your code that uses the mock object works, because you create an observable that immediately emits that one value.

    If authState is a subject: replace it with a ReplaySubject(1)

    If it is an observable, you need to publish is as a ReplaySubject like so:

    authState.pipe(
        publishReplay(1),
        refCount()
    );
    

    To fully understand what is going on, read this article: https://blog.mindorks.com/understanding-rxjava-subject-publish-replay-behavior-and-async-subject-224d663d452f

    Its a java article, but the same principles apply.

    But honestly, i cringe when i see people use the toPromise method :) You would learn rxjs much faster if you use it as intended!

    If i would write this code, it would look kinda like this:

    public save$: Subject<StoryForm> = Subject<StoryForm>();
    private destroy$: Subject<any> = new Subject();
    
    ngOnDestroy(): void {
        this.destroy$.next();
    }
    
    onInit() {
        // the (click) eventhandler in your GUI should call save$.next(storyForm)
        // this will trigger this statement
        this.save$.pipe(
            // withLatestFrom will fetch the last value from an observable, 
            // it still needs to be a ReplaySubject or ReplaySubject for this to work though!
            // it will pass an array down the pipe with the storyForm value, and the last value from authState
            withLatestFrom(this.auth.authState),
            // switchMap does the actual work: note that we do not return a value, 
            // but an observable that should that return a value soon, that is why we need switchMap!
            switchMap(([storyForm, authInfo]) => {
                // i am assuming the "set" method returns an observable
                // if it returns a Promise, convert it using fromPromise
                return this.afStore.doc(`stories/${key}`).set(story).pipe(
                    // catchError needs to be on your api call
                    catchError(err => console.log(err))
                );
            }),
            // this will kill your subscriptino when the screen dies
            takeUntil(this.destroy$)
        ).subscribe(value => {
            // "value" will be the return value from the "set" call
            this.router.navigate(['/stories']);
        }
    }