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$
:
- Begins with
fetch$
, meaning it will execute whenever fetch$
emits
exhaustMap
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.