2

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 Answers 2

1
  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

4
  • So when I add a new item to my data my app will then re-fetch the list which would not happen any more since it sees the call to the URL as being the same as it was last time, which it won't be. Is there a way around this or will I have to go back to maintaining my own internal cache property? Would I bet better off restructuring this to use a BehaviorSubject instead?
    – Chris Barr
    Commented Oct 25, 2023 at 20:25
  • edited the answer: if you add a new item you can call the refresh method and then the request would have to be executed again
    – dariosko
    Commented Oct 25, 2023 at 20:53
  • isn't this getAll an hot observable?
    – Grogu
    Commented Oct 25, 2023 at 21:03
  • 1
    I think this is probably the best and most correct answer, so I've marked it as correct, however I think based on what all I've learned here I am going to restructure things a bit to have a shared parent component that will make the request, the service sill store and emit the data list as a BehaviorSubject to any subscribers. Hopefully this way it will just keep the list current and I don't need to worry about caching at all.
    – Chris Barr
    Commented Oct 26, 2023 at 16:43
1

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.

1
  • Thanks yes this is probably a better way to do this vs the separate API call get get just a single item since I already have the full list
    – Chris Barr
    Commented Oct 26, 2023 at 16:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.