I'm new to RXJS and would to know if what I am doing is best practice. I have an API whihc returns a list of countries. I need to use this list in several different components. I have an external api that returns this, it can be very slow.
I have a service to call this API. In the constructor of this service I kick off the HTTP request which then passes the list to a BehaviorSubject. I need to use the unedited list in one component this subscribes the BehaviorSubject. Another component will use selected values from the list and I am using the getCountryById function to do this and return a single value in a string
Should I be using AsyncSubject to do this and is it bad practice to use this.CountriesSubject.value in my getCountryById function, should I also be using a BehaviorSubject.subscribe in there as well?
All of this does need to happen in the service
Thanks for your time :-)
export class CountryListService implements OnDestroy {
private baseUrl = 'api/countries/';
private CountriesSubject = new BehaviorSubject<country[]>([]);
Countries$: Observable<Country[]> = this.CountriesSubject.asObservable();
constructor(private http: HttpClient) {
this.getCountries();
}
getCountries() {
console.log('=======fetching Countries from API');
this.http.get<any>(this.baseUrl 'Countries').subscribe((data) => this.CountriesSubject.next(data?.countries));
}
getCountryById(id: any[]) {
if (!this.CountriesSubject.value) {
return undefined;
}
const country = this.CountriesSubject.value.find((f) => {
//Logic in here to get a country by and return its name
});
return country?.name;
}
ngOnDestroy() {
this.CountriesSubject.complete();
}
}
CodePudding user response:
For your case, I would suggest that you don't need Subject
at all. You can declare your list of countries$
as an observable and just share
it.
In general, I'd recommend to have your Angular services only return Observables, no imperative values.
export class CountryService {
private baseUrl = 'api/countries/';
countries$: Observable<Country[]> = this.http.get<Country[]>(this.baseUrl 'Countries').pipe(
shareReplay({refCount: false, bufferSize: 1})
);
constructor(private http: HttpClient) { }
getCountryById(id: string): Observable<Country> {
return this.countries$.pipe(
map(countries => countries.find(c => c.id === id))
);
}
}
Notes:
- lazy data: the http call will not get executed unless there is actually a subscriber, but the data will still be shared for subsequent subscribers
- getCountryById no longer needs to do a null check since it's defined from the
countries$
observable. It will just emit the value once it receives the list.
CodePudding user response:
Using value
on a BehaviourSubject is seen as a bad practice because you're breaking out of the reactivity (see this answer by Ben Lesh author of RxJS).
In short words : Subscribe
is your the best option.
This meens you should write a method that returns an observable :
getCountryById(id: string): Observable<Country> {
return this.countries$.pipe(
map(countries => countries.find(c => c.id === id))
);
}
CodePudding user response:
BehaviorSubjects are great for when you have multiple values being assigned over time. I use them for keeping track of something the user might change, like some sort of search filter where new values mean I need to go fetch new data from the api.
For simple operations, it is perfectly okay to assign the api data to a variable. Right now there is only ever one result that is going to be assigned.
getCountries() {
console.log('=======fetching Countries from API');
this.http.get<any>(this.baseUrl 'Countries')
.subscribe((data) => this.countries = data?.countries);
}
The way I would write the same thing would be to use the async pipe in the html. The real advantage is that if the user leaves the component it will unsubscribe to any outstanding api call.
<div *ngIf="countries$ | async as countries">
<!-- display countries here -->
</div>
The following would either display all countries or only the country that matches your id.
countries$: Observable<Country[]>;
private countryFilter$ = new BehaviorSubject<any[]>(null);
constructor(countryService: CountryService){
this.countries$ = combineLatest([
countryService.getAllCountries(), // contains the http call
this.countryFilter$,
]).pipe(
map((allCountries, filter)) => {
if (!filter) {
return allCountries;
}
const country = allCountries.find((f) => {
//Logic in here to get a country by and return its name
});
return [country?.name];
});
);
}
getCountryById(id: any[]) {
this.countryFilter.next(id);
}
With the combineLatest you will still only call the http once, and then you can send as many different values to look by id as you'd like and it will update the result.
Edit - I try to avoid subscribing in a service. You don't know when the observable has returned a value and error handling can be more complicated.