Angular service data caching with RxJS

147 Views Asked by At

I have an angular service that is specific to a type of data in my project. Right now it just passes everything straight trhough to a generic data service that handles the HTTP requests.

@Injectable({
  providedIn: 'root',
})
export class QuestionLibraryService {
  private readonly dataSvc = inject(DataService);

  getAll(): Observable<Array<IQuestionLibrary>> {
    return this.dataSvc.questionLibraryGetAll();
  }

  getOne(libraryId: string): Observable<IQuestionLibrary> {
    return this.dataSvc.questionLibraryGet(libraryId);
  }
  
  //create, delete, update, etc...
}

I want to cache the data received by this service to make fewer HTTP calls when navigating around the app, and to speed it up so that there aren't to many brief flashes of a loading state.

Here is what I have tried so far, and this works well for the getAll() method, but I'm not sure what to do about getOne().

private dataCache: Array<IQuestionLibrary> = [];

getAll(): Observable<Array<IQuestionLibrary>> {
  if (this.dataCache.length > 0) {
    return of(this.dataCache);
  }

  return this.dataSvc.questionLibraryGetAll().pipe(
    tap((libList) => {
      this.dataCache = libList;
    }),
  );
}

getOne(libraryId: string): Observable<IQuestionLibrary> {
  const found = this.getAll().pipe(
    map(list => list.find(item => item.id === libraryId))
  );

  //This is not right...
  if (found) {
    return found;
  }
}

The getOne() should get all of the items so they can be cached, this is a change from the current behavior where it calls a separate URL to get get a single item. I'm fine with abandoning that in favor of this.

However right now found is of type Observable<IQuestionLibrary | undefined> and I do not know hoe to check if an item was actually found or not since it's an observable.

I need it to either return the single found item, or to throw an error. How can I make it do this? Also, am I even on the right track here for how to cache data like this in a service?

2

There are 2 best solutions below

4
On BEST ANSWER
  private refresh$ = new BehaviorSubject<void>(undefined);

  getAll$(): Observable<Array<IQuestionLibrary>> {
    return this.refresh$.pipe(
      switchMap(() => {
        return this.dataSvc.questionLibraryGetAll().pipe(
          // catch error when nothing found - works only when service http request throws also error when nothing found - you can use catchError() also in DataService
          catchError((error) => {
            return throwError(error);
          })
        );
      }),
      // returns always the last value - no other caching needed
      shareReplay(1)
    );
  }

  refresh() {
    this.refresh$.next();
  }

catchError
https://rxjs.dev/api/operators/catchError

shareReplay https://rxjs.dev/api/operators/shareReplay Good post about shareReplay: https://careydevelopment.us/blog/angular-use-sharereplay-to-cache-http-responses-from-downstream-services

2
On

The answer of @oksd covers well, in my opinion, getAll.

When it comes to getOne, I think you should simply do this

getOne(libraryId: string): Observable<IQuestionLibrary> {
  return this.getAll().pipe(
    map(list => {
          const found = list.find(item => item.id === libraryId)
          if (found) {
              return found;
          }
             throw error('not found') // or something else, depending on your logic
    })
  );
}

This way getOne returns an Observable that can be consumed the same way you consume the result returned by getAll, e.g. via pipes or subscribe in your components.