55

I'm new to observables in angular. I have a problem where I want to return a value inside a subscribe method. I have the following method (getFirebaseData(idForm:string):observable <any[]>):

getTotalQuestions(idForm:string){
let totalQuestions:number;
this.getFirebaseData(idForm+"/Metadatos")
.subscribe(items => 
  {
    items.map(item => {
      totalQuestions=item.Total;
      console.log(totalQuestions);
    });
  }
);
console.log(totalQuestions);
return totalQuestions;
}

the first console.log(totalQuestions) prints 4 but the second console.log(totalQuestions) prints undefined. I understand that subscribe is an asynchronous operation and for that reason the second console.log(totalQuestions) ("In order to write the code") prints undefined, but I can not find the way to return the variable after the subscribe method has been completed. Now, if I change the subscribe to map:

getTotalQuestions(idForm:string){
let totalQuestions:number;
this.getFirebaseData(idForm+"/Metadatos")
.subscribe(items => 
  {
    items.map(item => {
      totalQuestions=item.Total;
      console.log(totalQuestions);
    });
  }
);
console.log(totalQuestions);
return totalQuestions;
}

the first console.log(totalQuestions) does not print anything and the second console.log(totalQuestions) prints undefined. It's something that I do not understand why it happens.

I hope you can help me clarify the concept that I do not understand. Thanks!

raysincanes
  • 73
  • 2
  • 8
AlejoDev
  • 4,345
  • 9
  • 36
  • 67
  • Please check the following [URL](https://stackoverflow.com/questions/38291783/how-to-return-value-from-function-which-has-observable-subscription-inside?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa) – Deepender Sharma Apr 02 '18 at 04:29

4 Answers4

77

You can't directly return totalQuestions like that, you need to use a subject to achieve that.

getTotalQuestions(idForm:string): Observable<string> {
let totalQuestions:number;
var subject = new Subject<string>();
this.getFirebaseData(idForm+"/Metadatos")
.subscribe(items => {
    items.map(item => {

      totalQuestions=item.Total;
      console.log(totalQuestions);
      subject.next(totalQuestions);
    });
  }
);
  return subject.asObservable();
}

Usage: getTotalQuestion(idForm).subscribe((r)=>console.log(r))

joey8oro
  • 97
  • 1
  • 3
  • 14
Rajani Kanth
  • 1,395
  • 10
  • 8
  • 1
    We must understand that what I was trying to do was an antipattern, can't force an asynchronous task to be synchronous, for that reason, the methods that include an observable usually return another observable or return nothing. Therefore, I accept this answer as a solution. Thank you very much! – AlejoDev Apr 02 '18 at 13:59
  • @Rajani Kanth Can you check this problem too? It is as same as the above question! https://stackoverflow.com/questions/54873434/get-subscribe-data-out-of-subscribe-in-angular – Nabid Feb 27 '19 at 15:53
  • You the champ! Thanks – Muhammad Noman Jul 26 '19 at 13:12
  • Thanks! It seems crazy that with all the rxjs methods, there is none that work like a deferred promise in angularjs. This works great though. – brett Feb 23 '21 at 12:41
  • @Rajani: its working but i want to return inside subscribe data from service instead of component can you tell me how to do that? – Kapil Soni Oct 05 '21 at 05:36
  • It worked perfectly, Thanks. It is not my case that I do not use the map section, only the first reply from the subscriber would serve me, but I was obliged, it was the same as what I was specifying. – Philip Developer Jan 05 '22 at 00:00
4

To solve this I would create a property totalQuestions$ an observable

In the TS file

totalQuestions$ = this.getFirebaseData(idForm + "/Metadatos").pipe(
  map(items => items.reduce((prev, {Total}) => prev + Total), 0)))
)

And Now in your template you can use async pipe

<span>{{ totalQuestions$ | async }}</span>

If you would like to use the variable in your TS file, then you can use combination operators like forkJoin or combineLatest to access this value

newVariable$ = combineLatest([totalQuestions$, ...]).pipe(
  map(([totalQuestions, ... ]) => {
    // Perform an operation here and return a value
  })
)
Owen Kelvin
  • 14,054
  • 10
  • 41
  • 74
2

Observable runs when you subscribe to it and what return from subscription is always a subscription object like setInterval. first sample: because this is a async call, second console.log won't wait for subscribe to finish before it executes.

getTotalQuestions(idForm: string) {
  let totalQuestions: number;
  return this.getFirebaseData(idForm + "/Metadatos").pipe(
     map(items =>
      items.map(item => {
        totalQuestions = item.Total;
        console.log(totalQuestions);
      });
    ));
}

getTotalQuestions('231').subscribe(console.log)
Andy Birchall
  • 311
  • 1
  • 3
  • 12
Fan Cheung
  • 10,745
  • 3
  • 17
  • 39
1

I made a live example. You do not need to fire the subscription to make transformations to the array or reduce some object to a single value, here is my approached. Your service

getTotalQuestions(idForm: string): Observable<any> {
    return this.getFirebaseData(`${idForm}/Metadatos`)
        .pipe(map(items => items
            .map(item => item.Total)
            .reduce((a, b) => a+b), 0));
}

Your component

this.service.getTotalQuestions('id')
    .subscribe(totalQuestions => console.log(totalQuestions));
Luillyfe
  • 6,183
  • 8
  • 36
  • 46