Implementing Caching in Angular 5 Services

Searching for the best way to implement Angular services has led me here.

The Service:

const url = 'http://127.0.0.1:8000/api/brands/'

@Injectable()
export class BrandService {

  private brands:Observable<Array<Brand>>;

  constructor(private http: Http) { }

  list(): Observable<Array<Brand>> {
    if(!this.brands){
      this.brands = this.http.get(url)
                          .map(response => response.json())
                          .publishReplay(1)
                          .refCount();
    }    
    return this.brands;
  }

  clearCache() {
    this.brands = null;
  }

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(url+'create/', formData)
      .map(response => response.json())
      .catch(this.handleError);
  }

  get(id): Observable<Brand> {
    return this.http.get(url+id)
        .map(response => response.json())
        .catch(this.handleError);
  }

  private handleError(error:any, caught:any): any {
    console.log(error, caught);
  }
}

I've successfully implemented caching using the publishReplay method with the Observable object. Now, my goal is to automatically update the list every minute and notify subscribers if there are changes in the list. I attempted to use setInterval(this.clearCache, 1000*60) but it only clears the cache without updating the list as desired.

What would be the most effective approach to stay updated on data while limiting server requests?

UPDATE 1 (Validator Issue):

Following martin's suggestion, I made changes to the list method:

list(): Observable<Array<Brand>> {
    if(!this.brands){
      this.brands = Observable.timer(0, 60 * 1000)
                        .switchMap(() => {
                          console.log('REQUESTING DATA....')
                          return this.http.get(url);
                        })
                        .map(response => response.json())
                        .publishReplay(1)
                        .refCount();
    }
    return this.brands;
  }

This new implementation works well, except for validators.

The brandNameValidator previously worked fine:

private brandNameValidator(control: FormControl) {
    return this.brandService.list().map(res => {
      return res.filter(brand => 
            brand.name.toLowerCase() === control.value.toLowerCase() && (!this.editMode || brand.id != this.brand.id)
        ).length>0 ? { nameAlreadyExist: true } : null;
    });
}

However, the field now remains in PENDING status.

UPDATE 2 (Validator Solution):

To resolve the issue, I employed the Promise object:

private brandNameValidator(control: FormControl) {
    return new Promise(resolve => {
      let subscription = this.brandService.list().subscribe(res => {
        let brandsFound = res.filter(brand => 
          brand.name.toLowerCase() === control.value.toLowerCase() && (!this.editMode || brand.id != this.brand.id)
        )
        if (brandsFound.length>0) {
          resolve({ nameAlreadyExist: true });
        } else {
          resolve(null);
        }
        subscription.unsubscribe();
      })
    });
}

UPDATE 3 (Forcing List Update):

After creating a new brand, I aim to immediately force an update of the list instead of waiting for the next scheduled update. This requires notifying all observers about the addition without utilizing the next() method since the object is an Observable and not a Subject.

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(url+'create/', formData)
      .map(response => {
        // TODO - Need to update this.brands, but I cannot use the next() method since it isn't a Subject object, but an Observable.
        // All observers need to be updated about the addition
        return response.json();
      })
      .catch(this.handleError);
  }

Answer №1

Here is one way to achieve the desired result:

Observable.timer(0, 60 * 1000)
  .switchMap(() => this.http.get(endpoint))
  .map(response => response.json())
  .publishReplay(1)
  .refCount();

Answer №2

Personally, my preferred approach for managing service state involves utilizing an elm/flux/redux/ng-rx pattern.

In response to your question, I can say that this is how I would handle it, although there may be aspects in the get() and create() functions that need further testing.

type Brand = string // or potentially a more intricate object...
type State = { brands: Brand[]/*, someMoreInfo: any*/ }

// Definition of Actions that can alter the state
type UpdateFn = (state: State) => State 

class MyService implements OnDestroy {
  updater$: Subject<UpdateFn> // Used for updating the state
  state$: BehaviorSubject<State> // Listens for state updates

  // Listens for updates to the brands (within state$)
  brands$: Observable<Brand[]> 

  // Initiate the first update (if necessary before the first minute)
  firstUpdate$ = new Subject<void>()

  autoUpdateSub = null

  constructor(initialValue = { brands: []/*, someMoreInfo: {}*/ }) {

    this.state$ = new BehaviorSubject<State>(initialValue)
    this.brands$ = this.state$.pluck('brands')

    this.updater$ = new Subject<UpdateFn>()
    const dispatcher = (state: State, op: UpdateFn) => op(state)

    // Where the magic happens
    // scan and dispatcher execute the Action function received on the 
    // last state and generate a new state that is sent inside the state$
    // subject (everyone that has subscribed to state$ will receive the 
    // state update).
    this.updater$.scan(dispatcher, initialValue).subscribe(this.state$)

    this.autoUpdateSub =
      //Force update on the first list() or every minute (not perfect)
      Observable.merge(
        Observable.interval(60 1000),
        this.firstUpdate$.take(1) 
      ).subscribe(_ => this.forceUpdate())

  }

  ngOnDestroy() {
    if (this.autoUpdateSub) this.autoUpdateSub.unsubscribe()
  }


  forceUpdate(): Observable<Brand[]> {
    console.log('update')
    this.http.get(endpoint)
      .map(response => response.json())
      .map((brands: Brands[]) => {
        // provide a function returning the updated brand list
        return previousState => {
          const newState = previousState
          newState.brands = brands
          return newState
        }
      })
      .subscribe(brandsUpdateFn => this.updater$.next(brandsUpdateFn))

    return this.brands$;
  }

  list(): Observable<Brand[]> {
    console.log('list')
    this.firstUpdate$.next()
    return this.brands$
  }

  get(id): Observable<Brand> {
    // retrieve the brands (send a brand request or access cache)
    return this.brands$
      .switchMap(brands => {
        // find the brand based on index (needs to be implemented)
        const index = findBrandByIndex(id, brands)

        // if brand exists, return it within an observable
        // otherwise, make a request
        return index ? Observable.of(brands[index])
          : this.http.get(endpoint + id)
            .map(response => response.json())
      })
      .catch(this.handleError);
  }

  create(brand: Brand): Observable<Brand> {
    Object.entries(brand).forEach(([key, value]) => {
      formData.append(key, value);
    });
    return this.http.post(endpoint + 'create/', formData)
      .map(response => response.json())
      //Optimistic response : add the brand to the store, and force refetch data
      .do(_ => {
        // Add temporarily to the store
        this.updater$.next((previousState) => {
          const newState = previousState
          newState.brands = [...previousState.brands, brand]
          return newState
        })
        // refetch values from server
        this.forceUpdate()
      })
      .catch(this.handleError);
  }

  private handleError(error:any, caught:any): any {
    console.log(error, caught);
  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for successfully importing $lib in SvelteKit without encountering any TypeScript errors

Is there a way to import a $lib into my svelte project without encountering typescript errors in vscode? The project is building and running smoothly. import ThemeSwitch from '$lib/ThemeSwitch/ThemeSwitch.svelte'; The error message says "Canno ...

Retrieve the value of a property with a generic type

Within our angular6 project, we encountered an issue while using the "noImplicitAny": true setting. We realized that this caused problems when retrieving values from generic types. Currently, we retrieve the value by using current['orderBy'], bu ...

Reactive form allows you to easily format dates

Currently, the date displayed is 1/4/2022. We need it to display in the format 01/04/2022. Can we achieve this formatting using reactive forms with the sample Model form provided below? Thank you. How can we format it when starting from transactionStartD ...

Incorporating Swagger-UI into an Angular 10 project

Looking to integrate Swagger UI into your Angular application? After researching extensively, I found that the recommended approach is now to use the swagger-ui package instead of swagger-ui-dist for single-page applications. For those using React, there ...

Creating an overloaded callable interface using TypeScript

The thread on implementing a callable interface provides some helpful information, but it doesn't fully address my specific query. interface lol { (a: number): (b: number) => string // (a: string): (b: string) => string // overloaded wi ...

Learn how to display data from the console onto an HTML page using Angular 2

I am working on a page with 2 tabs. One tab is for displaying active messages and the other one is for closed messages. If the data active value is true, the active messages section in HTML should be populated accordingly. If the data active is false, th ...

Extract all objects from an array where a specific field contains an array

data:[ { id:1, tags:['TagA','TagB','TagC'] }, { id:2, tags:['TagB','TagD'] }, { id:3, tags:[&a ...

Issue: Unable to locate a differ that supports the object '[object Object]' of type 'object'. NgFor can only bind to Iterables like Arrays

I have successfully pulled data from the jsonplaceholder fake API and now I am attempting to bind it using Angular 2 {{}} syntax. However, I encountered an error that states: "Error: Cannot find a differ supporting object '[object Object]' of typ ...

Beware: The use of anonymous arrow functions in Next.js can disrupt Fast Refresh and lead to the loss of local component state

I am currently encountering a warning that is indicating an anonymous object in a configuration file, and even specifying a name for it does not resolve the warning. Below you will find the detailed warning message along with examples. Warning: Anonymous ...

Running into a glitch while trying to npm install the Angular 2 quickstart project

Having trouble installing the dependencies for this repository. The issue arises during the postinstall script while trying to run the typings install command The error message displayed is: typings ERR! message Unable to read typings for "es6-shim". Yo ...

Can we verify if this API response is accurate?

I am currently delving into the world of API's and developing a basic response for users when they hit an endpoint on my express app. One question that has been lingering in my mind is what constitutes a proper API response – must it always be an o ...

Is it necessary to import the same .less file into every Angular component template?

Within my angular cli project, the setup includes: angular-cli.json: "styles": [ "styles/styles.less" ], styles.less: @import 'general'; general.less: .pointer { cursor: pointer; } In the component's styles .less file, ...

Is there a method to reference a class before it has been defined?

I have a working code snippet that currently appears like this: import { OtherClass } from "other-class" class VeryLongClass { } OtherClass.run(VeryLongClass); However, I would like to rearrange it to look like this: import { OtherClass } fro ...

What is the best way to display inner HTML in an Angular MatAutoComplete MatOption when a user makes a selection?

My challenge lies in displaying a list of properties that contain HTML tags for superscript or subscript in an Angular Material MatAutoComplete list. While I can successfully showcase these values, the issue arises when trying to display a user's sele ...

Querying data elements using Graphql mutations: a step-by-step guide

const MUTATION_QUERY = gql` mutation MUTATION_QUERY( $name: bigint! ) { insert_name( objects: { name: $name } ) { returning { id name } } } `; const [onClick, { error, data }] = useMut ...

Experimenting with retrieving input from other components while implementing setTimeout

In continuation of the previous question (linked here), I am still working on tutorials for Angular testing using the same files. The current issue revolves around the setTimeout function. Within both ngOnInit and ngAfterViewInit, I have included a setTim ...

How can I combine multiple requests in RxJS, executing one request at a time in parallel, and receiving a single combined result?

For instance, assume I have 2 API services that return data in the form of Observables. function add(row) { let r = Math.ceil(Math.random() * 2000); let k = row + 1; return timer(r).pipe(mapTo(k)); } function multiple(row) { let r = Math.c ...

What is the reason behind Jest's failure with the error message "component is a part of the declaration of 2 modules"?

While running a test in an Angular (NX) project, I encountered the following exception: Error: Type FooComponent is part of the declarations of 2 modules: FooComponentModule and FooComponentModule! Please consider moving FooComponent to a higher module tha ...

Tips for successfully sending an interface to a generic function

Is there a way to pass an interface as a type to a generic function? I anticipate that the generic function will be expanded in the future. Perhaps the current code is not suitable for my problem? This piece of code is being duplicated across multiple fil ...

A JavaScript function written without the use of curly braces

What makes a function good is how it is declared: export declare class SOMETHING implements OnDestroy { sayHello() { // some code to say hello } } However, while exploring the node_modules (specifically in angular material), I stumbled up ...