30
votes

Before I start my question, I'd like to let you know that I already did a big research, and I cannot find a solution ( explanation ) why I get this error.

Please also note, that I am totally new on Angular, and I just started to learn how it works.

So, the problem I have is what I have entered in the title of this question.

What I try to do, is to build a login system using the Firebase, based on a course I purchase on Udemy.

The code I use is the following:

auth.service.ts

import {Injectable} from '@angular/core';
import * as firebase from 'firebase';

@Injectable ()
export class AuthService {
    token: string;

    // ...

    singInUser ( email: string, password: string ) {
        // login process here ...
    }

    // Responsible to retrieve the authenticated user token
    getToken () {   
        return firebase
            .auth ()
            .currentUser
            .getIdToken ();
    }
}

data-storage.service.ts

// ... Dependencies here
@Injectable ()
export class DataStorageService {
    private recipeEndPoint: string = 'https://my-unique-id.firebaseio.com/recipes.json';
    private recipeSubscription: Observable<any> = new Observable();

    constructor ( private http: Http,
                  private recipes: RecipeService,
                  private authService: AuthService ) {}

    // other functionality ...

    getRecipes () {
        const token = this.authService.getToken ();

        token.then (
            ( token: string ) => {
                this.recipeSubscription = this.http.get ( this.recipeEndPoint + '?auth=' + token ).map (
                    ( data: Response ) => {
                        return data.json ();
                    }
                );

                // THIS PARTICULAR CODE WORKS AS EXPECTED
                // WITH NO ISSUES
                this.recipeSubscription.subscribe (
                    ( data: Response ) => {
                        console.log ( 'Data response: ', data );
                    },
                    ( error ) => {
                        console.log ( 'Error: ' + error );
                    }
                )
            }
        );

        // This is supposed to return an Observable to the caller
        return this.recipeSubscription;
    }
}

header.component.ts

// Dependencies here ...

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
  constructor(private dataStorage: DataStorageService, private recipeService: RecipeService) { }

  // Other Code Here ...

  onFetchData() {
    let recipeSubscription = this.dataStorage.getRecipes();

    // THIS RETURNS TRUE
    console.log(recipeSubscription instanceof Observable);

    // THIS LINE THEN RETURNS THE MESSAGE:
    // ERROR TypeError: Cannot read property 'subscribe' of undefined
    recipeSubscription.subscribe();

    // IF I COMMENT OUT THE PREVIOUS LINE
    setTimeout(
      () => {
        // THIS RETURNS TRUE
        console.log(recipeSubscription instanceof Observable);
      },
      500
    );

    setTimeout(
      () => {
        // AS WELL THIS ONE RETURNS TRUE
        console.log(recipeSubscription instanceof Observable);
      },
      1000
    );

    setTimeout(
      () => {
        // AS WELL THIS ONE RETURNS TRUE
        console.log(recipeSubscription instanceof Observable);
      },
      1500
    );
  }
}

So, unfortunately, I cannot see what could be wrong with this code. Can anyone spot anything I did wrong?

Note: I have removed parts of my code just for make the snippets more readable. If you need any other part, please feel free to ask me, and I will provide it here.

UPDATE #1

This is how it looks like the header.component.html

<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">Logo Here</div>

        <div class="navbar-default">
            <ul class="nav navbar-nav">
                <!-- Left Navigation Options -->
            </ul>
            <ul class="nav navbar-nav navbar-right">
                <!-- Right Navigation Options -->
                <li class="dropdown" appDropdown>
                    <a routerLink="/" class="dropdown-toggle" role="button">Manage <span class="caret"></span></a>
                    <ul class="dropdown-menu">
                        <li>
                            <a style="cursor: pointer;" (click)="onSaveData()">Save Data</a>
                        </li>
                        <li>
                            <!-- Here is where I call the onFetchData method -->
                            <a style="cursor: pointer;" (click)="onFetchData()">Fetch Data</a>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </div>
</nav>
9
from where do you call the onFetchData() method in your header.component.ts ?AlexWoe89
In data-storage servcie you have already subscribed.Adam A
The onFetchData() it is called from the component HTML using the code (click)="onFetchData()"KodeFor.Me
It is probably problem because on that same observable you are subscribing two times.Adam A
Probably problem is in other place but in the best of my knowledge using subscribe more than once is wrongAdam A

9 Answers

33
votes

I got the same error from using an unitialized EventEmitter:

@Output() change: EventEmitter<any>;

instead of:

@Output() change: EventEmitter<any> = new EventEmitter<any>();

The error occurred in the higher-level component that tried to subscribe to the change event.

19
votes

The issue seems to be the order in which your code gets executed, more specifically the getRecipes() method :

// Numbers indicate the execution order

getRecipes () {
    const token = this.authService.getToken ();

    // 1. You call a promise, which will take a while to execute...
    token.then (
        ( token: string ) => {
            // 3. Finally, this bit gets executed, but only when the promise resolves.
            this.recipeSubscription = ...
        }
    );

    // 2. Then, you return a variable that hasn't been assigned yet,
    // due to the async nature of the promise.
    return this.recipeSubscription;
}

The solution to this is that your getRecipes () method SHOULD NOT SUBSCRIBE. It should return either a Promise or an Observable.

Something like this:

getRecipes() {
    // Convert the initial promise into an observable
    // so can you use operators like map(), mergeMap()... to transform it.
    const tokenObs = Observable.fromPromise(this.authService.getToken());

    // Merge the token observable into an HTTP observable
    // and return the JSON data from the response.
    return tokenObs
      .mergeMap(token => this.http.get('XXX?auth=' + token))
      .map(resp => resp.json());
}

Then, the calling code in HeaderComponent becomes :

const recipeObs = this.dataStorage.getRecipes();
recipesObs.subcribe(jsonData => {
  // Use the JSON data from the HTTP response
});

Several remarks:

  • You need to explicitly import the RxJS operators used in your code. If you follow my example, you need to add the following imports at the beginning:
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
  • You should NEVER subscribe in the method that creates the observable. In your case, don't subscribe in getRecipes(). ALWAYS subscribe at the last minute possible. You can subscribe multiple times to the same observable, but be aware that each subscription re-executes the observable (in the case of an http request, it means you run the request multiple times; not ideal...).
  • It is not a good idea to call your variable recipeSubscription since it contains an Observable, not a Subscription. A subscription is what subscribe() returns. In other words: const subscription = observable.subscribe().
  • I see that you're using the Firebase SDK directly. Are you aware of AngularFire library?
6
votes

Problem

I stumble upon the same error and the reason was that I was initializing my @Output event emitter inside ngOnInit().

export class MyClass implements OnInit {

    @Output()
    onChange : EventEmitter<void>;

    ngOnInit() {
        // DO NOT initialize @Output event here
        this.onChange = new EventEmitter<void>();    
    }
}

Solution

When I changed the initialization to the same place of the declaration it worked.

export class MyClass implements OnInit {

    @Output()
    onChange : EventEmitter<void> = new EventEmitter<void>();

    ngOnInit() {
    }
}

I think this happens because the parent component tries to subscribe to the event too soon (before ngOnInit() is triggered).

4
votes

The problem is, you're returning an observable and re-assigning it in the response of Token().

Try making a Subject of the Observable you have now, i find these easier to use.

public recipeSubscription: Subject<any> = new Subject();

Change your assignment from

this.recipeSubscription = this.http.get....

To

let response = this.http.get....

Subscribe on that within the function this gets called:

response.subscribe((res) => {this.recipeSubscription.next(res)})

Now you can subscribe directly on the property

this.dataStorage.recipeSubscription.subscribe((res) => {
    // Do stuff.
});

this.dataStorage.getRecipes();

I hope this is enough to help you :)

0
votes

Ionic 4.

I had a method that subscribes to an observable, calling that method within ngOnInit, raised this error. Moving the method call to constructor solved my problem.

0
votes

As other answers already mentioned you should return observable in order to subscribe it. In my case it was throwing error despite that. I just restarted my angular app with

ng serve 

and then it worked properly.

0
votes

I am using angular8, Had similar challenge. I was supposed to hit an endpoint with some valid credentials appended to my get/post, thus calling the methods before checking the credentials caused the problem.

Making sure that you're authenticated before any action should solve the problem:

  ngOnInit() {
    this.authService.isAuthenticated().then(status => {
      if (status === true) {
      this.getClients();
      this.getCities();
     } else {
    this.routerCtrl.navigate(["login"]);
   }
 });
}
0
votes

I was getting that same error where it was subscribing to an undefined value, so what I did was use an if statement to check that there's actually a value to subscribe to. Like so:

if(this.memberService.getSetMember() !== undefined) {
    this.memberService.getSetMember().subscribe((value: any) => this.member = value)                
}
0
votes

I was able to solve this issue by binding the observable value to the subject.

observable = behaviourSubject.asObservable();

I added this in the constructor.

Below is the skelton of the code.

  subjectVar: BehaviorSubject<type> = new BehaviorSubject<type>({} as type)
  observableVar: Observable<type>

  constructor(){
     this.observableVar = this.subjectVar.asObservable();
  }
  
  pulic function()Observable<type>{
      foo();
      return observableVar

  }

  foo(){
     let var = someValue
     subjectVar.next(var)
  }