How to synchronise http.client request observable using url suffix drawn from storage

HI there,

(first time posting - please go easy on me!).

I’m struggling with what I imagine might be a simple problem to fix hitting a wall here if anyone can help please.

I have an ionic app which asks the user to input a city, and that is put into storage. This is on another page, I have it so that the stored value will be added to the prefix of a url string and called in a http request to an API.

While both work individually, when trying to get them to work together I’m having difficult. I’m not sure whether its a question of relying on lifecycle hooks or using synchronous keywords. I’ve tried playing with both but I really think I’m not quite understanding each as both either to not update or draw an error.

At the moment, I’ve housed the code in a provider.

Any help?

@Injectable()
export class CitiesProvider {

  constructor(public http: HttpClient, private storage: Storage) {
 
  }

url: string = "https://restcountries.com/v3.1/capital/";
cityCap: string;



    getCities(): Observable<any>{
	
  this.storage.get("cityChoice")
.then((val) => {
console.log
	this.cityCap = val;
})
.catch((error) => {
	alert("Error accessing Storage")
});

  return this.http.get(this.url + this.cityCap );
  
  }
}

Hi

the very short answer - definitely no pun intended - is that the storage.get runs independently from the return http statement. So basically this will always fail to achieve your goal of having this.cityCap filled with proper content prior to running of this.http.get

Basically you are assuming code to run in the order of appearance (synchronous), whereas they are asynchronous.

There are multiple solutions. The dirtiest being placing an await in front of the storage get (and async infront of getCities).

There are cleaner ways, like having getCities actually not return an observable but a promise and then chaining both. Requiring you to do a toPromise() on the http get. Or turn the storage get into an Observable requiring you then to switchmap both calls.

I think there will be more options including code proposals.

Hope this helps

Where it should stay, IMHO.

Some heuristics I have found keep me from stepping on rakes:

  • no any unless absolutely necessary (which it almost never is, and certainly not here - give getCities a proper return type)
  • don’t use device storage for in-app communication - storage shouldn’t be involved here at all unless the city is going to stay set across runs of the app
  • if the city is quasi-permanent, only read from storage once per app run. definitely not every time getCities is called
  • never rely on lifecycle events for business object data management
  • do not expose bare scalars from providers (cityCap is bad)
  • naming things is very important
  • think reactively, not imperatively

That last one was no doubt the absolute hardest one for me to internalize. I had decades of telling computers “do this, then do that”. Trying to write web apps that way is a recipe for constant pain. With web apps, you are putting together a supply chain or building a road: you write a bunch of glue code that gets you from point A to point B, but the less that code knows about anything outside of “we’re at point A now” the better off you’ll be.

With that in mind, let’s rethink our design here. I’m concerned about naming - a CitiesProvider provides cities. Yet we have a single city already chosen. Getting many cities from one city is a mess for readability. Judging from your URL, what getCities really does is find the closest capital city to some arbitrary city. If that’s not the case, adjust the naming as required, but taking the time to name things accurately will pay off for you hundreds of times over as you or others try to understand the code in the future.

closestCapital should take everything it needs:

interface City {
  id: string;
  name: string;
  ...
}

class CityProvider {
  private homeCityName$ = new Subject<string>();
  
  constructor(private http: HttpClient, private storage: Storage) {
    storage.get("homeCity").then(home => this.homeCityName$.next(home));
  }
  
  watchHomeCityName(): Observable<string> {
    return this.homeCityName$;
  }

  pokeHomeCityName(hcn: string): void {
    this.storage.set("homeCity", hcn);
    this.homeCityName$.next(hcn);
  }

  closestCapital(targetCityName?: string): Observable<City> {
    let target$: Observable<string> = targetCityName !== undefined ?
        of(targetCityName) : this.homeCityName$;
    return target$.pipe(switchMap(target => this.http.get(this.url + target)));
  }
}