I am trying to display to users a list of 'reports' they can add which requires a combination of multiple AJAX requests and observables -- one request to an api to find the 'applications' they can access, and then multiple requests to a defined endpoint for each application. Once this step has been completed, I never need to make the ajax requests again.
I have something working but I do not really understand it and I feel like there should be a much easier way to do this.
I have some working code right now, however, I find it overly complicated and hardly understand how I got it to work.
private _applications: ReplaySubject<Application>;
private _applicationReports: ReplaySubject<ApplicationReports>;
// Ajax request to fetch which applications user has access to
public fetchApplications(): Observable<Application[]> {
return this._http.get('api/applications').pipe(
map(http => {
const applications = http['applications'] as Application[];
applications.forEach(app => this._applications.next(app));
this._applications.complete();
return applications;
})
);
}
// Returns an observable that contains all the applications
// a user has access to
public getApplications(): Observable<Application> {
if (!this._applications) {
this._applications = new ReplaySubject();
this.fetchApplications().subscribe();
}
return this._applications.asObservable();
}
// Returns an observable which shows all the reports a user has
// from all the application they can access
public getApplicationReports(): Observable<ApplicationReports> {
if (!this._applicationReports) {
this._applicationReports = new ReplaySubject();
this.getApplications().pipe(
mergeMap((app: Application) => {
return this._http.get(url.resolve(app.Url, 'api/reports')).pipe(
map(http => {
const reports: Report[] = http['data'];
// double check reports is an array to avoid future errors
if (!reports || !Array.isArray(reports)) {
throw new Error(`${app.Name} did not return proper reports url format: ${http}`);
}
return [app, reports];
}),
catchError((err) => new Observable())
);
})
).subscribe(data => {
if (data) {
const application: Application = data[0];
const reports: Report[] = data[1];
// need to normalize all report urls here
reports.forEach(report => {
report.Url = url.resolve(application.Url, report.Url);
});
const applicationReports = new ApplicationReports();
applicationReports.Application = application;
applicationReports.Reports = reports;
this._applicationReports.next(applicationReports);
}
}, (error) => {
console.log(error);
}, () => {
this._applicationReports.complete();
});
}
return this._applicationReports.asObservable();
}
Expected functionality:
When a user opens the 'Add a report' component, the application starts a series of ajax calls to fetch all the applications the user has, and all the reports from those applications. Once all the ajax request are completed, the user sees a list of reports they can choose to add. If the user opens the 'Add a report' component a second time, they already have the report list and the application doesn't need to send more ajax requests.
The answer depends on the overall architecture of your application. If you are a following a Redux-style architecture (ngrx in Angular), then you can solve this problem by caching the API responses in the store, i.e., LoadApplicationsAction => Store => Component(s).
In this flow, your requests to load the list of applications and the details for each only occur once.
If you are not implementing such an architecture, then the same principles still apply, but your constructs/implemenation changes. Per your code example, you're on the right track. You can either shareReplay(1) the response of fetchApplications that essentially replays the most recent value emitted by the source to future subscribers. Or similarly, you can store the results in a BehaviorSubject which achieves a similar result.
Regardless of whether you choose to implement ngrx or not, you can simplify your Rx code. If I understand your expected results, you are only trying to show the user a list of Report objects. If that is the case, you can do that like this (untested but should get you to the right result):
public fetchApplicationReports(): Observable<Report[]> {
return this._http.get('api/application').pipe(
mergeMap(apps => from(apps).pipe(
mergeMap(app => this._http.get(url.resolve(app.Url, 'api/reports'))
),
concatAll()
)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With