Bill Headrick Bill Headrick - 4 years ago 121
Javascript Question

Angular2 / Typescript knowing when http has been fetched or an observable has updated

I have an angular2 and typescript app where I am using angular2's http methods to load data from a database inside a service. I trigger the service inside a component during its onInit(). This works fine and I am able to load the data. The problem is that I want also want to use that data that is loaded from the service inside the onInit() function. When I try to do this, I get an error similar to the one below:

Error: Uncaught (in promise): TypeError: Cannot read property 'user_id' of undefined
TypeError: Cannot read property 'user_id' of undefined


Here is reduced code for the component calling the service

export class ProfileComponent implements OnInit {

public profile: StaffProfile[];

constructor(private userService: UserService) {}

ngOnInit() {
this.userService.fetchProfile();
this.profile = this.userService.getProfile();
//I just want to be able to do anything once the data is loaded
console.log(this.profile[0].user_id);
}
}


Here is the reduced code of the service

@Injectable()
export class WorkforceUserService implements OnInit {

private Profile: Profile[];

constructor(private http: Http) {
this.Profile = [];
}

public getProfile(){
return this.Profile;
}

public fetchStaffProfile(){
return this.http.get('http://localhost:3000/api/staff/1')
.map((response: Response) => response.json())
.subscribe(
(data) => {
var user_id = data.user_id || null;

var loadedProfile = new Profile(user_id);

this.Profile.push(loadedProfile);
}
);
}
}


All I want is to be able to trigger a function in my component when the data from the server has arrived, or just updated. Please let me know your thoughts on how I can accomplish this.

Thank you in advance.

Answer Source

A classic scenario of involving sync & async worlds. (TL;DR - My suggested solutions are below)

So, this is the flow you expect when ngOnInit() runs:

1. (Component) Ask the service to fetch the profile  
2. (Service) Fetch the profile  
3. (Service) Extract the user_id from the profile received and create new profile  
4. (Service) Push the profile into this.Profile
5. (Component) Set this.profile as service's Profile
6. (Component) Print profile's first entry that was fetched and configured in the service.

The flow that you actually get is:

1 => 2 => 5 => 6 (fails, but hypothetically) => 4 => 5.

In the synchronous world:

  • Fetch method runs and returns a Subscription to the http call and in this point, fetch method is done. Right after, ngOnInit continues with this.profile = this.userService.getProfile();

At the same time, in the asynchronous world:

  • the http request is executed, and sometime in the future will populate this.Profile.

But, before it happens, ngOnInit tries to access to a property user_id of an undefined first item element.

So, what you need in such case is to stay in the asynchronous world, and in this area rxjs provides pretty cool and well documented toolset to handle such cases.


My suggestions are:

Naive solution - Instead of returning a subscription, fetch method will return a Promise, which will be resolved in ngOnInit.

// WorkforceUserService

public fetchStaffProfile() {
    return this.http.get('http://localhost:3000/api/staff/1')
        .map((response: Response) => response.json())
        .toPromise()
        .then((data) => {
            var user_id = data.user_id || null;
            var loadedProfile = new Profile(user_id);
            this.Profile.push(loadedProfile);
        });
       // trying to explain my point, don't forget to catch promise errors
}

// ProfileComponent

ngOnInit() {
    this.userService.fetchProfile().then(() => { 
        // this lines are called when http call was done, as the promise was resolved
        this.profile = this.userService.getProfile();
        console.log(this.profile[0].user_id);
    });

}

Rxjs style solution - Hold a Subject with type of profile array, which the the component will subscribe to:

// WorkforceUserService

this.Profile = new Subject<Profile[]>(); // the subject, keep it private and do not subscribe directly
this.Profile$ = this.Profile.asObservable(); // expose an observable in order to enable subscribers.

public fetchStaffProfile(){
    return this.http.get('http://localhost:3000/api/staff/1')
        .map((response: Response) => response.json())
        .subscribe(
            (data) => {
                var user_id = data.user_id || null;
                var loadedProfile = new Profile(user_id);
                this.Profile.next([loadedProfile]);
    });
}

// ProfileComponent

export class ProfileComponent implements OnInit {

    public profile: StaffProfile[];

    constructor(private userService: UserService) {
        // here you can subscribe to the Profile subject, and on each call to 'next' method on the subject, the provided code will be triggered
        this.profile = this.userService.getProfile();
        console.log(this.profile[0].user_id);  
    }

    ngOnInit() {
        // here, we'll ask the service to start process of fetching the data.
        this.userService.fetchProfile();
    }
}

Regardless to your questions, some points that might help:

  1. this.Promise => this.promise is better in terms of js naming conventions.
  2. var is oldschool. use let or const instead. see angular styleguide

This article might shed some light on using observables, with a detailed example and explanatins.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download