How to Ensure a BehaviorSubject always has a value?

Viewed 437

I'm writing this application in Angular and I'm using Resolvers.

Once the user loads a given page, there is an API-Endpoint that will be hit by the resolver and one of the parameters that needs to be passed is the user's country. To get that info, I'm using the ip-api.com API.

Here is my problem. Because I'm using Observables, I can't get the country location on time, before the request is out. I need to have the user's country first and then send the request.

My idea was to create a service that gets injected in the root, so as soon as the application starts its constructor executes a method to fetch the user location data. Then, I store that data on a BehaviorSubject so it can be accessed by any class that needs it later.

How can I ensure that I will always have the data I need before any request goes out?

If the user comes from the main page everything works well, but if the user goes directly to a secondary page, then I get an error.

Here is what I got:

Helper Service

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { take } from 'rxjs/operators';

@Injectable( { providedIn: 'root' } )
export class HelperService {
    public showRightDrawer = new BehaviorSubject<boolean>(false);
    public requestCountry = new BehaviorSubject<string>(undefined);

    constructor(private http: HttpClient) {
        this.detectUserLocation();
    }

    detectUserLocation(): void {
        const url = 'http://ip-api.com/json';
        this.http.get(url).pipe(take(1)).subscribe((c: any) => {
            const cc = c.countryCode;
            this.requestCountry.next(cc);
            return cc;
        });
    }
}

Service that hits my internal API

import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { SERVER_URL } from 'src/environments/environment';
import { HelperService } from '../common/services/helper.service';
import { Roaster } from './roaster.interface';

@Injectable()
export class RoasterService {
    private endPoint: string = SERVER_URL;
    private country: string;
    private httpOptions = {
        headers: new HttpHeaders({
            'Content-Type': 'application/json',
            responseType: 'text'
        })
    };

    constructor(private httpService: HttpClient, private helper: HelperService) {
        helper.requestCountry.subscribe((c) => this.country = c);
    }

    getRoasters(page?: number, limit?: number): Observable<Roaster[]> {
        const reqLimit = limit || 15;
        const reqPage = page || 0;
        return this.httpService
          .get<Roaster[]>(`${this.endPoint}roaster?limit=${reqLimit}&page=${reqPage}&country=${this.country.toLowerCase()}`, this.httpOptions )
          .pipe(take(1));
    }
}
1 Answers

In general, it's best not to subscribe in your services. Observables are lazy by nature, and subscribing in the constructor of a service causes data to be fetched regardless of whether or not a consumer is subscribing to it.

You can expose your data in your HelperService as an observable without subscribing like this:

export class HelperService {

    constructor(private http: HttpClient) { }

    private url = 'http://ip-api.com/json';

    public countryCode$ = this.http.get(this.url).pipe(
        map(response => response.countryCode),
        shareReplay()
    );
}

You see we use map to transform the response to the relevant property. We use -shareReplay so that multiple subscribers will share the same subscription and receive an already received value. Now, consumers can subscribe to countryCode$ to get the value when they need it, but the service isn't initiating the data flow, the consumer is.

Now, in your RoasterService, the trick is to return an observable that first references countryCode$, then makes your http call with that data:

export class RoasterService {

    private endPoint: string = SERVER_URL;

    constructor(private httpService: HttpClient, private helper: HelperService) { }

    getRoasters(page?: number, limit?: number): Observable<Roaster[]> {
        const reqLimit = limit || 15;
        const reqPage = page || 0;

        return this.helper.countryCode$.pipe(
            switchMap(cc => this.httpService.get<Roaster[]>(... use cc in here ...))
        );
    }

}

You can see we define the response of the getRoasters() method to start with helpers.countryCode$ then we pipe that result into switchMap which will internally subscribes to our http.get() observable (when getRoasters is subscribed to) and emits its response.

Related