18
votes

In Angular 2 v2.0.1 the onInit is called twice. (Obviously I'm also doing something wrong when it's called once, but that's not the issue right now)

Here's my Plunker: http://plnkr.co/edit/SqAiY3j7ZDlFc8q3I212?p=preview

Here's the service code:

import {Injectable} from '@angular/core';
import {Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';

@Injectable()
export class DemoService {


  constructor(private http:Http) { }

  // Uses http.get() to load a single JSON file
  getData() {
    return this.http.get('./src/data.json')
      .map((res:Response) => res.json())
      .do(data => console.log(data))
      .subscribe(data => {
        return <PageContent[]>data;
      }, error => console.log("there was an error!"));
  }
}

export class PageContent {
  constructor(public _id: string, 
  public tag: string, 
  public title: string, 
  public body?:string, 
  public image?: string) {}
}

... and the simple component that uses it.

//our root app component
import {Component, NgModule, OnInit } from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { DemoService, PageContent } from './service';
import { HttpModule } from '@angular/http';

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
    </div>
    <div *ngFor="let page of pages">
      {{ page.title }}
    </div>
  `
})
export class App implements OnInit {
  name:string;
  pages: PageContent[] = [];

  constructor(private _service: DemoService) {
    this.name = 'Angular2'
    this.loadData();  // <-- this is called once
  }

  ngOnInit() {
    //this.loadData();  // <-- this is called twice 
  }

  loadData(){
    this.pages = this._service.getData();
    console.log(this.pages);
  }
}

@NgModule({
  imports: [ BrowserModule, HttpModule ],
  declarations: [ App ],
  providers: [DemoService],
  bootstrap: [ App ]
})
export class AppModule {}

Disclaimer: it's erroring out, but you can see it's being served once when the service method is being called from the constructor, but it gets called twice when it's inside the ngOnInit hook.

My question is, why is it being called twice from the OnInit function?

UPDATE: solution from all answers:

This is the new service method:

getData() {
    return this.http.get('./src/data.json')
        .map((res:Response) => res.json() as PageContent[]);
}

... and this is the new component method:

loadData(){
    this._service.getData()
        .subscribe(data => this.pages = data);
}
4
Remove the part of the template using ngFor and causing the app to fail, and you'll see that it's called only once, in normal circumstances.JB Nizet
@JBNizet - but I need the *ngFor to display the data.King Wilder

4 Answers

14
votes

Your subscribe should be put in the component instead of the service. Reason being your component is subscribed to the data returned from the service, and later on you can unsubscribe or add more control (such as denounce) if needed. The code will look like this after the changes.

In your component:

  ngOnInit() {
    this.loadData();
  }



  loadData(){
    this._service.getData().subscribe(data => this.pages = data);
  }

In you service:

  getData() {
    return this.http.get('./src/data.json')
      .map((res:Response) => res.json());
  }
3
votes

this._service.getData() returns a Subject, not a PageContent list. You could change your loadData like :

loadData() {
  this._service.getData().subscribe(data => this.pages = data);
  console.log("Load data !");
}

and remove the subscribe part of your getData method (from DemoService). I've just tested this, and the ngOnInit is called once

2
votes

In English, when you subscribe to a stream (Observable) the code inside the first function inside the subscribe block will be executed when that observable emits data.

If you subscribe twice it will be called twice, etc

Since you are subscribing multiple times the first function (called the next function) inside the subscribe block will be executed multiple times.

You should only subscribe to a stream once inside ngOnInit.

When you want to emit data onto the stream, you could use an RXJS Subject for this and make the Subject subsequently emit to the stream you are subscribed to using RXJS flatmap.

1
votes

ok my second answer ... see if this helps ...

return subject.asObservable().flatMap((emit: any) => {

  return this.http[method](url, emit, this.options)
    .timeout(Config.http.timeout, new Error('timeout'))
    // emit provides access to the data emitted within the callback
    .map((response: any) => {
      return {emit, response};
    })
    .map(httpResponseMapCallback)
    .catch((err: any) => {
      return Observable.from([err.message || `${err.status} ${err.statusText}`]);
    });
}).publish().refCount();

where subject is an RXJS subject that you are emitting to (with the subject.next() method)