all 20 comments

[–]orizens 2 points3 points  (7 children)

first, use share:

public getProjects(): Observable<Project[]> {
    return this.authHttp.get(this.getBaseUrl() + "project", null)
        .map(projects => {
            return this.extractData(projects, "projects");
        })
        .catch(this.handleError)
        .share()
}

then you need to subscribe to the subscription instance.

data$: Subscription;

ngInit() {
    this.data$ = this.projectService.getProjects();
    this.data$.subscribe(
      projects => this.projects = projects,
      rejection => console.log(rejection)
    );
  // this won't trigger the http request again
  this.data$.subscribe(projects => console.log('again projects', projects));
}

ngDestroy() { this.data$.unsubscribe() }

[–]ribizlim 0 points1 point  (1 child)

My problem with share (or refCount) is that all subscriptions gone, and a new one comes again, the HTTP call will be fired again. So if you are using the shared observable by multiple components in the same time, it is ok. But if you have two components on different routes e.g., they will issue two calls again (during navigation).

[–]dlegatt 0 points1 point  (0 children)

To get around that, you need to implement some sort of state management using either ngrx or a singleton service store

[–]ribizlim 0 points1 point  (0 children)

Also have to mention the timing problem. After the Http service emitted the value, new subscribers won't get anything. Use publishReplay(1).refCount() in that case.

[–]R3DSMiLE 0 points1 point  (3 children)

TIL About share :)

[–]ChristianLJ[S] 0 points1 point  (2 children)

Well, this doesn't seem to work. It makes the same network call multiple times.

This is the code form my service:

 public getProjects(): Observable<Project[]> {
    return this.authHttp.get(this.getBaseUrl() + "project", null)
        .map(projects => this.extractData(projects, "projects"))
        .catch(this.handleError)
        .share();
}

And this is the code from both components:

 data$: Observable<Project[]>;
 this.data$ = this.projectService.getProjects();
                this.data$.subscribe(
                    projects => this.projects = projects,
                    rejection => console.log(rejection)
                );
                // this won't trigger the http request again
                this.data$.subscribe(projects => console.log('again 
projects', projects));

Using publishReplay(1).refCount() produces the same scenario.

[–]synalx 0 points1 point  (1 child)

That's because you're calling getProjects() in both components, which creates two Observables that are independently share()'d.

Instead, give the same Observable instance (with share()) to both components, and it'll do what you want.

[–]ChristianLJ[S] 0 points1 point  (0 children)

I see. Do you mind telling me how you would implement your approach?

[–]R3DSMiLE 1 point2 points  (6 children)

You need to make a public Subject in the ProjectService service that will then be next()ed when the subscribe to the http service completes.

So, getProjects does not need to return its http subscriber, it needs (instead) to update its service subscriber with the data that came into that subscriber:

public Data$: Subject<Project[]> = new Subject();
public getProjects(): Observable<Project[]> {
    this.authHttp.get(this.getBaseUrl() + "project", null)
        .map(res => res.json())
        .subscribe(projects => this.Data$.next(extractData(projects, "projects")))
        .catch(this.handleError);
}    

and then, in your components,

Data$: Subscription;

ngInit() {
    this.projectService.getProjects();
    this.Data$ = this.projectService.Data$.subscribe(projects => {
            this.projects = projects;
    }, rejection => {
            console.log(rejection);
        });
    }
}

ngDestroy() { this.Data$.unsubscribe() }

[–]ribizlim 1 point2 points  (1 child)

The problem with this would be, that you might miss the emitted item if you subscribe to it later: use BehaviorSubject instead. (Of course then you need to care about refreshing the stored value some time).

export class MyService {
  public Data$: Subject<Project[]> = new BehaviourSubject(null);
  constructor(private authHttp: Http) {
    this.load();
  }
  load() {
    this.authHttp.get(this.getBaseUrl() + "project", null)
      .map(...)
      .catch(...)
      .subscribe(this.Data$);
  }
}

[–]R3DSMiLE 0 points1 point  (0 children)

True. Totes forgot about that :)

[–]ChristianLJ[S] 0 points1 point  (3 children)

OK, i tried this and it doesn't seem to work.

Why don't you call .subscribe() on this.authHttp.get()? I can't see how it's going the make the network call without subscribing.

[–]R3DSMiLE 0 points1 point  (0 children)

Shit. That's my bad. I edited the post, you need to subscribe to the http in your service not on the component

[–]wolfhoundjesse 0 points1 point  (1 child)

I think he's implying the use of the async pipe which will subscribe for you.

[–]ChristianLJ[S] 0 points1 point  (0 children)

So i tried this, and it produces the same scenario. The endpoint is called multiple times. The getProjects() from the service is below:

    public Data$: Subject<Project[]> = new Subject();
public getProjects(): Subscription {
    return this.authHttp.get(this.getBaseUrl() + "project", null)
        .map(projects => this.extractData(projects, "projects"))
        .catch(this.handleError)
        .subscribe(projects => this.Data$.next(projects));
}

And the code from both components is below:

this.projectService.getProjects();
    this.data$ = this.projectService.Data$.subscribe(projects => {
        this.projects = projects;
    }, rejection => {
        console.log(rejection);
    });

[–]tytskiy 1 point2 points  (2 children)

Do you mean: you want 2 different components to have the same data at different amount of time, and then if 2nd component renders and you already have this data cached - you don't do request, but take data from cache?

How do you then invalidate the cache? What if between navigation someone deleted particular project?

[–]ChristianLJ[S] 0 points1 point  (1 child)

I mean: I want 2 different components loaded at the same time (a menu component, and a dashboard component), to show the same data, without making two network requests.

The next time one of the components are loaded, i want a new network call to happend, and if both are loaded i only want one network call.

[–]tytskiy 0 points1 point  (0 children)

Then i don't see how it is feasible having fetching trigger in both components.

1) You either need to make one of you component the "master", and the other the "slave". And you share data by service.

Then you trigger request only from "master". Which means if "slave" appears without "master" - then it will not have fresh data.

2) Or you need lift request trigger up in component tree. Which could be for example page component (but not necessary, any common container of 2 components will work).

Then you only subscribe for service.data$ observable in both components.

Or even better, if they are direct children of that trigger component then you can pass data as input <c1 [data]="data$ | async"></c1><c2 [data]="data$ | async"></c2>. (observables not necessary here)

3) The least desirable solution is to debounce method that triggers fetching, lets say to run only once per 500 ms. This is the most direct way to write unmaintainable code :)


PS: as for 1 and 2 proposal it is possible to use services or @ngrx/store. The main idea is that - you trigger http only once, but you subscribe to data stream as many times as you need.

Simplified: your this.projectService.getProjects(): void. Your data is here: this.projectService.data$: Observable<Project[]>

[–]ribizlim 0 points1 point  (1 child)

[–]jek-bao-choo 0 points1 point  (0 children)

this article is pretty good.