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.