Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular "Cannot read property 'subscribe' of undefined"

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>
like image 489
KodeFor.Me Avatar asked Oct 24 '17 08:10

KodeFor.Me


4 Answers

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.

like image 78
Mark Wheeler Avatar answered Nov 16 '22 03:11

Mark Wheeler


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?
like image 38
AngularChef Avatar answered Nov 16 '22 03:11

AngularChef


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).

like image 6
Arthur Silva Avatar answered Nov 16 '22 05:11

Arthur Silva


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 :)

like image 5
Benjamin Bloot Avatar answered Nov 16 '22 04:11

Benjamin Bloot