Reputation: 801
In my angular service I have a cached http request which is called once when my component is initialized
//variables
private readonly URL: string = 'http://makeup-api.herokuapp.com/api/v1/products.json';
private cachingSubject: ReplaySubject<IProduct[]>
private _products: IProduct[] = [];
//method
public getAllProducts(): Observable<IProduct[]> {
if (!this.cachingSubject) {
this.cachingSubject = new ReplaySubject<IProduct[]>(1);
this.http.get<IProduct[]>(this.URL)
.subscribe((productsData: IProduct[]) => {
this._products = productsData;
this.cachingSubject.next(productsData);
});
}
return this.cachingSubject.asObservable();
}
and my component when makes a service method:
public products: IProduct[] = [];
private destroySubject$: Subject<void> = new Subject<void>();
ngOnInit(): void {
this.productService
.getAllProducts()
.pipe(takeUntil(this.destroySubject$))
.subscribe((products: IProduct[]) => {
this.products = products;
})
}
public onForceReload(): void {
//need to reload onClick
}
The question is, how can I force reload (reinit cache) from my cached request ? Make getAllProducts again
Upvotes: 4
Views: 1819
Reputation: 6706
You can achieve that like the following:
cachedRequest$
to assign to it the HTTP request and pipe
it with shareReplay
, which will cache the response and return it every time you subscribe to the observable.cachedRequest$
if has a value, or assign the HTTP request (with shareReplay
) if not.forceReload
parameter to your function to force reload, and if it's true, just set the cachedRequest$
to null, to be reinitialized again.Try the following:
cachedRequest$: Observable<IProduct[]>;
public getAllProducts(forceReload = false): Observable<IProduct[]> {
if (forceReload) this.cachedRequest$ = null;
if (!this.cachedRequest$) {
this.cachedRequest$ = this.http.get<IProduct[]>(this.URL).pipe(
tap(productsData => (this._products = productsData)),
shareReplay(1)
);
}
return this.cachedRequest$;
}
Then in your component, it's better to handle the request in a different way to avoid multi subscribe to the source observable, so you can do something like the following:
products$: Observable<IProduct[]>;
ngOnInit(): void {
this.loadProducts();
}
public onForceReload(): void {
//need to reload onClick
this.loadProducts(true);
}
private loadProducts(forceReload = false) {
this.products$ = this.productService.getAllProducts(forceReload);
}
Then in your component template use it like the following:
<ng-container *ngIf="products$ | async as products">
<!-- YOUR CONTENT HERE -->
</ng-container>
Upvotes: 1
Reputation: 14740
I think the simplest way to achieve what you are looking for is to use a single Subject as a trigger to fetch the data. Then define an allProducts$
observable on the service (instead of a method) that depends on this "fetch subject".
Provide a simple refreshProducts()
method that calls .next()
on the fetch subject, which will cause allProduct$
to refetch the data.
Consumers can simply subscribe to allProducts$
and receive the latest array of products. They can call refreshProducts()
to reload data. There will be no need to reassign references to subjects or observables.
export class ProductService {
private fetch$ = new BehaviorSubject<void>(undefined);
public allProducts$: Observable<IProduct[]> = this.fetch$.pipe(
exhaustMap(() => this.http.get<IProduct[]>('url')),
shareReplay(),
);
public refreshProducts() {
this.fetch$.next();
}
}
Here's a working StackBlitz demo.
Here's the flow for allProducts$
:
fetch$
, meaning it will execute whenever fetch$
emitsexhaustMap
will subscribe to an "inner observable" and emit its emissions. In this case that inner observable is the http call.shareReplay
is used to emit the previous emission to new subscribers so the http call isn't made until refreshProducts()
is called (this isn't required if you will only have 1 subscriber at a time, but generally in services, its a good idea to use it)The reason we define fetch$
as a BehaviorSubject
is because allProducts$
will not emit until fetch$
emits. A BehaviorSubject
will initially emit a default value, so it causes our allProducts$
to execute without the user needing to click the reload button.
Notice there is no subscription happening in the service. This is a good thing because it allows our data to be lazy, meaning we aren't just fetching just because some component injected the service, we only fetch when there is a subscriber.
Also, this means our service has a unidirectional data flow, which makes things a lot easier to debug. Consumers only get data by subscribing to public observables and they modify the data by calling methods on the service... but these methods do NOT return data, only cause it to be pushed through the observables. There is a really good video on this topic featuring Thomas Burleson (former Angular team member).
You see I used the name allProducts
for the exposed observable instead of getAllProducts
. Since allProducts$
is an observable, the act of subscribing implies the "get".
I like to think of observable as little data sources that will always push the latest value. When you subscribe, you are listening for future values, but the consumer isn't "getting" them.
I know that was a lot, but I have one more little tid-bit of advice that I think cleans up code quite a bit.
This is the use of the AsyncPipe
.
So your component code could be simplified to this:
export class AppComponent {
public products$: Observable<IProduct[]> = this.productService.allProducts$;
constructor(private productService: ProductService) { }
public onForceReload(): void {
this.productService.refreshProducts();
}
}
And your template:
<ul>
<li *ngFor="let product of products$ | async">{{ product }}</li>
</ul>
Notice here there's no need to subscribe in the template just to do this.products = products;
. The async pipe subscribes for you, and more importantly unsubscribes for you. This means, you don't need the destroy subject anymore!
Here's a StackBlitz fork of the previous updated to use async pipe.
Upvotes: 4