17

I'm currently working on an Angular 2 project with AngularFire2 and I'm trying to convert a FirebaseListObservable to a Promise. I know that it doesn't make much sense since Observables are more useful but this function will be part of another function where multiple promises are chained. And I'm not familiar with how to subscribe to Observables within a chain of promises... The function gets executed in the service, however it seems to not return anything. Basically, what I want to do is check in a Firebase list if an object with a certain name already exists and return either true or false.

Service

constructor(private _af: AngularFire) { }

nameExists(name: string): Promise<boolean> {

 return this._af.database.list('users')
  .map(users => {

    let exists = false;
    users.forEach(user => {
      if(user.name.toLowerCase() === name.toLowerCase()) {
        console.log('Name already exists!');
        exists = true;
      }
    });
    return exists;
  }).toPromise();
}

component

constructor(private _usersService: UsersService) { }

check(name) {
 this._usersService.nameExists(name)
  .then(bool => console.log(bool));
}

So the function gets executed and seems to work correctly as it prints to the console, when there's a match. However, console.log() in the component does not get executed. I guess the "then" part is never reached. On a separate note, is there a way to stop the forEach loop once a match is found?

Any help would be greatly appreciated as I couldn't find any answers at all to this.

user276648
  • 6,018
  • 6
  • 60
  • 86
Leonidas
  • 195
  • 1
  • 5

2 Answers2

35

The problem is that the toPromise operator converts the observable to a promise that resolves to the observable's final value. That means the observable must complete before the promise resolves.

In AngularFire2, list and object observables don't complete; they re-emit whenever the database changes.

You can solve the problem using the first operator, which takes the first emitted value and then completes the composed observable:

import 'rxjs/add/operator/first';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
...
return this._af.database
  .list('users')
  .map(users => {

    let exists = false;
    users.forEach(user => {
      if (user.name.toLowerCase() === name.toLowerCase()) {
        console.log('Name already exists!');
        exists = true;
      }
    });
    return exists;
  })
  .first()
  .toPromise();
cartant
  • 57,105
  • 17
  • 163
  • 197
  • 4
    Just when you thought you've used Observables enough to understand them... Thank you so much! It worked! – Leonidas Jan 27 '17 at 00:38
  • When using the .first(), is the subscription lost? – Luis Ruiz Figueroa Feb 06 '18 at 00:06
  • 1
    @Luis No, `first` is a 'normal' operator and a `subscribe` call is still required. However, `toPromise` isn't. It involves an implicit call to `subscribe`, as it converts the observable into a promise. The returned promise resolves when the observable completes. Also, when an observable completes, all subscribers are automatically unsubscribed. – cartant Feb 06 '18 at 00:34
  • why first and not `take(1)` for example? – Jimmy Kane Dec 23 '18 at 17:59
  • 3
    @Jimmy If the source completes without emitting a value, `first` will error - `take(1)` won't. See https://stackoverflow.com/a/42346203/6680611. Unless I expect the source to sometimes complete without emitting, I favour using `first`. – cartant Dec 23 '18 at 22:10
  • Where can I read more to fully understands this? if Firebase never `completes` (it keeps emitting `success`) then the promise will never `resolve` ?!! – Rami Alloush Jun 03 '20 at 03:50
0

I solved this by unsubscribing it after the first data received lazy way :)

const userSubs$ = this.db.object(`branch/${branchId}/products`).subscribe(
                     async (payload: any) => {
                      userSubs$.unsubscribe();
                       })

Or convert your code to something like this good way :)

 const eventref = this.db.database.ref(`branch/${branchId}/products`);
  const snapshot = await eventref.once('value');
  const productArray = snapshot.toJSON()
  return productArray;

Happy Coding

Divek John
  • 623
  • 8
  • 16