Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fetch data once with Observables in Angular 2

I have a service, what is used several times from a lot of my Angular 2 components. It fetches customer data from a Web API and returns an Observable:

getCustomers() {
   return this.http
        .get(this.baseURI + this.url)
        .map((r: Response) => {
            let a = r.json() as Customer[];                       
            return a;                         
        });               
}    

I inject this service in my root component, and in every component that wants to access the customers I just subscribe to that Observable:

this.customerService.getCustomers().subscribe(v => this.items = v);

However, every component who subscribes to my Observable causes another execution of the HTTP-request. But to fetch the data only once is enough. If I try share(), it does not solve my problem:

getCustomers() {
   return this.http
        .get(this.baseURI + this.url)
        .map((r: Response) => {
            let a = r.json() as Customer[];                       
            return a;                         
        }).share();               
}   

Still the same issue. Any proposals which operators I have to use to only fetch data once?

like image 371
David Avatar asked Nov 10 '16 14:11

David


People also ask

What is .next in Angular?

next() The subject next method is used to send messages to an observable which are then sent to all angular components that are subscribers (a.k.a. observers) of that observable.

What is difference between Promise and Observable in Angular 2?

Promises deal with one asynchronous event at a time, while observables handle a sequence of asynchronous events over a period of time. Emit multiple values over a period of time. Emit a single value at a time. Are lazy: they're not executed until we subscribe to them using the subscribe() method.

Which is better observables or promises?

the Promise is always asynchronous, while the Observable can be either asynchronous or synchronous, the Promise can provide a single value, whereas the Observable is a stream of values (from 0 to multiple values), you can apply RxJS operators to the Observable to get a new tailored stream.


2 Answers

1) You can simply save downloaded data in your service:

export class CustomersService {
  protected _customers: Array<Customer>;

  constructor(public http: Http) {}

  public getCustomers(): Observable<Array<Customer>> {
    return new Observable(observer => {
      if (this._customers) {
        observer.next(this._customers);
        return observer.complete();
      }
      this.http
        .get(this.baseURI + this.url)
        .map((r: Response) => (r.json() as Array<Customer>))
        .subscribe((customers: Array<Customer>) => {
          this._customers = customers;
          observer.next(this.customers);
          observer.complete();
        });
    });
  }
}

2) Shorter approach taking refresh parameter:

export class CustomersService {
  protected _customers: Array<Customer>;

  constructor(public http: Http) {}

  public getCustomers(refresh?: boolean): Observable<Array<Customer>> {
    if (!refresh && this._customers) {
      return Observable.of(this._customers);
    }
    return this.http
            .get(this.baseURI + this.url)
            .map((c: Response) => (c.json() as Array<Customer>))
            .do((customers: Array<Customer>) => {
                this._customers = customers;
            });
    });
  }
}

3) Taking advantage of ReplaySubject:

export class CustomersService {
  protected _customers$: ReplaySubject<Array<Customer>> = new ReplaySubject(1);
  protected _customersInitialized: boolean;

  constructor(public http: Http) {}

  public getCustomers(refresh?: boolean): Observable<Array<Customer>> {
    if (refresh || !this._customersInitialized) {
      this._customersInitialized = true;
      this.http
        .get(this.baseURI + this.url)
        .map((c: Response) => (c.json() as Array<Customer>))
        .subscribe((customers: Array<Customer>) => {
          this._customers$.next(customers);
        });
    }
    return this._customers$.asObservable().skip(+refresh).distinctUntilChanged();
  }
}

And then:

this.customersService.getCustomers()
    .subscribe(customers => this.customers = customers);

You can also expose the always up-to-date customers field from SomeService for read only purposes (like displaying in the templates) this way:

public get customers(): ReadonlyArray<Customer> {
  return this._customers;
}
like image 140
Daniel Kucal Avatar answered Oct 12 '22 12:10

Daniel Kucal


I would create a parent container, fetch the data once, and pass it to child components using @Input.

Parent:

@Component({
    selector: 'BarFooHttpCaller',
    template: ´<child *ngIf="data.length > 0" [data]></child>´
})

class BarFooHttpCaller {
    private data: any;
    constructor(private foobar:Foobar) {
        this.data = {};
    }

    ngOnInit() {
        this.foobar.getCustomers().subscribe(() => {       
            console.log('httpdone') 
        });
        this.foobar.dataStream.subscribe((data) => {
            console.log('new data', data);
            this.data = data;
        })
    }
}

Child:

import { Component, Input } from '@angular/core';

@Component({
    selector: 'child',
    template: ´<div>{{data}}</div>´
})

export class Child {
    @Input() data: any;

}
like image 23
Yoann Avatar answered Oct 12 '22 11:10

Yoann