Search code examples
angularhttptypescriptrxjsdry

How to make an API call in Angular HttpClient upon multiple possible triggers without subscribing to the API call method multiple times in my code?


I am making a mildly large Angular Cli app, I have a data table that has a lot of dynamic filters and a search bar and basically and whenever:

A) The search value changes
B) The pagination changes
C) A filter is removed or applied

I need to make an api call to get the updated table items and the updated filter info.

I have a function in my service that returns the response of the api call which looks something like this...

getFilters() {
    const body = { searchTerm: 'foo' }; // 
    return http.post(url, body)
        .map(res => res)
        .catch(err => Observable.of(err);
}

getTableItems() {
    const body = { searchTerm: 'foo', ...stuff for filtering }; // 
    return http.post(url, body)
        .map(res => res)
        .catch(err => Observable.of(err);
}

getApiData() {
    const filters = this.getFilters();
    const items = this.getTableItems();
    return forkJoin([ filters, items ]);
}

I also have a lot of other methods that get called, like I said, when pagination changes, or new search term, etc... Basically I'm having to put the getApiData().subscribe(res => doSomethingWithResponse(res)); in both the constructor, and each method that needs to trigger it like so

constructor() {
    ...constructor stuff;
    this.getApiData().subscribe(res => ...);
}
setPageSize() {
    ...stuff
    this.getApiData().subscribe(res => ...);
}
setPage() {
    ...stuff
    this.getApiData().subscribe(res => ...);
}
setSSearchTerm() {
    ...stuff
    this.getApiData().subscribe(res => ...);
}
... and so on...

My question is, how can I keep this DRY and still make the API call upon multiple triggers? Is there a way to deal with this issue? Note that I also use @ngrx/store (Redux implementation) for storing data on the Front End, and that I'm using rxjs Observable's and BehaviorSubject's in my code.


Solution

  • Posts from your API is one source of data in your application. There are sources of triggering events:

    1. search value
    2. pagination
    3. filters

    Let's start with printing out posts in your component. There will be some posts$ observable - a source of posts.

    <div *ngFor="let post in posts$ | async">{{post}}</div>
    

    Let's define such observable in your component.

    posts$: Observable<Post>;
    ngOnInit() {
        const body = {};
        this.posts$ = http.post(url, body);
    }
    

    Nice but we don't use any parameters, right?

    So what are our sources of changes? Let's define them as observables.

    posts$: Observable<Post>;
    page$: Observable<number>;
    searchTerm$: Observable<string>;
    otherFilters$: Observable<Filters>;
    

    Now let's define our sources of events in ngOnInit() hook. For sake of simplicity I will focus on pagination and search input only.

    ngOnInit() {
        // page is probably defined by URL
        this.page$ = this.route.paramMap
            .switchMap((params: ParamMap) => params.get('page'));
        // search term and filters will be probably from some form
        this.searchTerm$ = this.searchTerm.valueChanges
            .startWith('');
    
        // nothing changed here for now...
        const body = {};
        this.posts$ = http.post(url, body);
    }
    

    Alright now we want any change of page$ or $searchTerm to trigger new data download. Let's define our posts$ observable based on page$ and searchTerm$.

    this.posts$ = Observable.combineLatest(this.page$, this.searchTerm$, (page, searchTerm) => ({ page, searchTerm}))
        .switchMap(params => {
            let body = params;
            return http.post(url, body);
        });
    

    Here notice the last argument of combineLatest. It is transform function which is called every time something is emitted with values from each observable as arguments. I just create a single object which I will receive later in switchMap param.

    This is how you can compose observables and subscribe to the final source of data. When you use async pipe for subscription it deals with automatic unsubscribe so you dont have to manage it manually. Hope that helped. 👍