How to avoid unnecessary calling API when changing sub-routes with :id

Issue

I have a quite common scenario and I wish to know what would be the industry standard/best way to handle this problem.

The Problem

Let’s say you have multiple routes:

  • /organization
  • /organization/:id
  • /organization/:id/users
  • /organization/:id/users/:userId
  • /organization/:id/payments
  • … you get the drill

As you can see in this example everything revolves around Organization Id. To display all the information related to organization I need to call API.

Now the big issue is that I have these pages, but I really don’t want to be calling API on every single route. It feels very wrong to call getOrganizaionById on /organization/:id, then users navigates to /organization/:id/users and I have to call getOrganizaionById again (because, for example, I want to show organization name somewhere on the page).

What have I tried

Obviously I have some ideas in mind, I just want to ask some SPA/Angular pros to tell me what would be the better solution for my particular problem.

What I tried/can do is:

  • Memoize the getOrganizaionById – Not the best, also overly complicated if I want to do cache-busting (someone changes the organization details etc.)

  • Save selectedOrganizaion$ as a ReplaySubject inside of OrganizationService so that I know my currently selected organization – Feels wrong, also would have problems with caching, this would be essentially the same thing as memoization

  • Do something with Redux Store? By putting it in Redux Store wouldn’t it be the same as saving it inside of service as ReplaySubject? Also it is difficult to find anything regarding redux for this type of problem because I am not building a todo app.

I am lost and would like to have a correct approach for my problem because this should be a very common scenario.

Here is a stackblitz with exact problem ready to fiddle around with: https://stackblitz.com/edit/angular-bbhhoy

Solution

There are probably many ways to solve this.

Easiest

The easier would be to use ngx-cachable to decorate your service’s endpoints.
An example for your case would be:

organizations.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Cacheable } from 'ngx-cacheable';

@Injectable({providedIn: 'root'})
export class OrganizationService {

  constructor(private http: HttpClient) { }

  @Cacheable()
  public getOrganizations(): object[] {
    return this.http.get('organizations'); // or w/e your endpoint is
  }

  // see https://github.com/angelnikolov/ngx-cacheable#configuration for other options
  @Cacheable({
    maxCacheCount: 40 // items are cached based on unique params (in your case ids)
  })
  public getOrganizationById(id: number): object {
    return this.http.get(`organization/${id}`); // or w/e your endpoint is
  }

}

Pros:

  • The http call is only made once, and subsequent calls will return the cached value as an observable

Cons:

  • If you call getOrganizations() and load org 1 & 2, then call getOrganizationById(1), getOrganizationById will make the HTTP request for the organization again

Roll Your Own Cache

This is a little more work and could potentially be brittle (depending on how complex your data and services get).
This is just an example and would need to be fleshed out more:

import { Injectable } from "@angular/core";
import { of, Observable } from "rxjs";
import { delay, tap } from "rxjs/operators";

@Injectable({providedIn: 'root'})
export class OrganizationService {
  // cache variables
  private _loadedAllOrgs = false;
  private _orgs: IOrg[] = [];

  constructor() {}

  public getOrganizations(busteCache: boolean): Observable<IOrg[]> {
    // not the most verbose, but it works

    // if we haven't loaded all orgs OR we want to buste the cache
    if (this._loadedAllOrgs || busteCache) {
      // this will be your http request to the server
      // just mocking right now
      console.log("Calling the API to get all organizations");
      return of(organizationsFromTheServer).pipe(
        delay(1000),
        tap(orgs => this._orgs = orgs)
      );
    }

    // else we can return our cached orgs
    console.log("Returning all cached organizations");
    return of(this._orgs);
  }

  public getOrganizationById(id: number): Observable<IOrg> {
    const cachedOrg = this._orgs.find((org: IOrg) => org.id === id);

    // if we have a cached value, return that
    if (cachedOrg) {
      return of(cachedOrg);
    }

    // else we have to fetch it from the server
    console.log("Calling API to get a single organization: " + id);
    return of(organizationsFromTheServer.find(o => o.id === id)).pipe(
      delay(1000),
      tap(org => this._orgs.push(org))
    );
  }
}

interface IOrg {
  id: number;
  name: string;
}

const organizationsFromTheServer: IOrg[] = [
  {
    id: 1,
    name: "First Organization"
  },
  {
    id: 2,
    name: "Second Organization"
  }
];

Pros:

  • you have control over the caching
  • you don’t have to make subsequent calls to the backend if you already have the org in memory

Cons:

  • you have to manage the cache and busting it

Use a Redux-like Store

Redux is fairly complex. It took me several days to fully understand it. For most Angular apps, it is overkill to set up
the full redux system (in my opinion). However, I like having a central object or store to hold my app state
(or even parts of state). I use this implementation so much I finally just made a library so I could reuse it in my
projects. rxjs-util-classes specifically the BaseStore.

In the above example, you could do something like this:

organizations.service.ts

// other imports 
import { BaseStore } from 'rxjs-util-classes';

export interface IOrg {
  id: number;
  name: string;
}

export interface IOrgState {
  organizations: IOrg[];
  loading: boolean; 
  // any other state you want
}

@Injectable({providedIn: 'root'})
export class OrganizationService extends BaseStore<IOrgState> {
  constructor (private http: HttpClient) {
    // set initial state
    super({
      organizations: [],
      loading: false
    });
  }

  // services/components subscribe to this service's state
  // via `.getState$()` which returns an observable
  // or a snapshot via `.getState()`

  // this method will load all orgs and emit them on the state
  loadAllOrganizations (): void {
    // this part is optional, but if you are loading don't fire another request
    if (this.getState().loading) {
      console.log('already loading organizations. not requesting again');
      return;
    }

    this._dispatch({ loading: true });
    this.http.get('organizations').subscribe(orgs => {
      // this will emit the new orgs to any service/component listening to 
      // the state via `organizationService.getState$()`
      this._dispatch({ organizations: orgs });
      this._dispatch({ loading: false });
    });
  }

}

Then in your components you subscribe to the state and load your data:

organization-list.component.ts

// imports

@Component({
  selector: 'app-organization-list',
  templateUrl: './organization-list.component.html',
  styleUrls: ['./organization-list.component.css']
})
export class OrganizationListComponent implements OnInit {
  public organizations: IOrg[];
  public isLoading: boolean = false;

  constructor(private readonly _org: OrganizationService) { }

  ngOnInit() {
    this._org.getState$((orgState: IOrgState) => {
      this.organizations = orgState.organizations;
      this.isLoading = orgState.loading; // you could show a spinner if you wanted
    });
    // only need to call this once to load the orgs
    this._org.loadAllOrganizations();
  }

}

organization-single.component.ts

// imports...
import { combineLatest } from 'rxjs';

@Component({
  selector: 'app-organization-users',
  templateUrl: './organization-users.component.html',
  styleUrls: ['./organization-users.component.css']
})
export class OrganizationUsersComponent implements OnInit {
  public org: IOrg;

  constructor(private readonly _org: OrganizationService, private readonly _route: ActivatedRoute) { }

  ngOnInit() {
    // combine latest observables from route and orgState
    combineLatest(
      this._route.paramMap,
      this._org.getState$()
    ).subscribe([paramMap, orgState]: [ParamMap, IOrgState] => {
      const id = paramMap.get('organizationId);
      this.org = orgState.organizations.find(org => org.id === id);
    });
  }
}

Pros:

  • All components are always using the same organizations and state

Cons:

  • You still manually have to manage how you load your organizations into your OrganizationService state

That example isn’t fully fleshed out, but you can see how to implement a quick version of a Redux-like store
without implementing all of Redux’s patterns. The BaseStore acts as your single source of truth. It then exposes
methods that allows services and components the ability to interact with the state.

Another Option

There is another option that I have been working on to solve a similar issue in an app I am building.
I don’t have all of the details worked out so I won’t try to describe it here. Once I have the code finished,
I will update my answer.

TL;DR Version: Create a class that has a cache object and exposes a few methods for getting values off the “cache”
and/or watching changes on that “cache” (similar to the Redux example above). Components could then load all of the
“cache” or just one item.

Answered By – DJ House

Answer Checked By – Terry (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.