js-api-loader
js-api-loader copied to clipboard
Dynamically load map libraries
Hi all, This version of the map loader, like the previous one, does not provide the ability to incrementally and dynamically load libraries at different instance of time.
In my use case I have a single page app with forms which show an Autocomplete, a Map or a Map with drawing tools, depending on the selected route. I have modeled these control in reusable building blocks (e.g. React components, Angular directives, Web Elements, ...), but unfortunately each control requires a different subset of map library:
- autocomplete:
["places"] - map:
["places"], - map with drawing tools:
["places", "drawing"]
If I am not wrong, this loader should be able to guarantee 1-time-loading of libraries, so if I require "places", then I cannot load "drawing" later anymore. Basically the loader dies as soon as you use it (actually it may work if I use it multiple times, but I am not sure if the previously provided callback will be fired again).

The loader should take the ownership of handling multiple loading requests, also avoiding to load libraries which are already in place.
SBE:
const loader1 = new Loader({ apiKey, libraries: ["places"] });
await loader1.load(); // <-- loads "places" lib
const loader2 = new Loader({ apiKey, libraries: ["places", "drawing"] });
await loader2.load(); // <-- loads only "drawing" lib
The implementations is also quite easy, broadly like the following:
libraries: Libraries;
constructor(libraries: Libraries) {
// filter out already loaded services
const librariesToLoad = libraries.filter(library => this.libraries.indexOf(library) < 0);
// everything is loaded already?
if (librariesToLoad.length === 0) return;
// save reference to new libs
this.libraries.push(...librariesToLoad);
// create tag and load `librariesToLoad` libs
// ...
}
For what concern the callback to be provided in the URL, this can be dynamically provided with an incremental name:
window[`__googleMapsCallback_${++this.cbNo}`] = this.callback.bind(this);
url += `?callback=__googleMapsCallback_${this.cbNo}`;
Thanks, Indri
Thanks for reporting this use case. I'm not sure we will be able to support it though. If I make a request to load without any libraries followed by another with a library, I get the error about the API being loaded multiple times. It may work, but is probably fragile and could be broken in future releases. I'll dig a bit deeper internally to see what that error is actually about.
Here is a simple jsfiddle with two script loads demonstrating error: https://jsfiddle.net/jwpoehnelt/3ft4m92d/3/.
This issue(multiple loads) has been around for years. I would be concerned about any state tied to existing objects and billing implications. I'm think I am going to take a different route here albeit a longer term one and advocate for a public method to load additional libraries. Something like the following:
google.maps.load('places').then(...)
// or
google.maps.loadLibrary('places').then(...)
// or
google.maps.loadLibraries(['places']).then(...)
From a first glance at the internals, there is nothing fundamentally complex about this but will require some time for it to be released.
@jpoehnelt I would like to add one more use-case for dynamic load map - language switch.
I'm a maintainer of @react-google-maps/api https://www.npmjs.com/package/@react-google-maps/api I've released new version 1.12.0 with React hook based on your library.
I have my own script for loading googlemapsapi, and I'm seeing that with your script it is not possible to build url using googleMapsClientId and channel params.
One more request: could you please export type Libraries for usage? I had to copy it from your project to mine.
@JustFly1984
For the language request, can you file a feature request at https://issuetracker.google.com/issues/new?component=188853&template=787814.
Premium plans are now defunct and most of these will expire or be expiring shortly. No other users require client id and/or channel. I opened #69 for this.
@jpoehnelt I have created an issue, please follow up. https://issuetracker.google.com/issues/170657543
@jpoehnelt it turns out, there is already an issue exists https://issuetracker.google.com/issues/35819089
channel has been added to the latest release in #72.
any updates?
To answer OP's question, I ran in to the .load() method being deprecated, and the same issue with Autocomplete and Geocoding libraries only being needed on specific routes.
The function below can be imported and awaited anywhere, to check that window.google.maps exists in your global scope. You want to specify all the libraries you'll need to use throughout your application, so they all get appended to the maps URL.
const LoadGoogleMaps = async () => {
// Check if Google Maps has already been loaded
if (!window?.google?.maps) {
return await new Loader({
apiKey: process.env.GOOGLE_MAPS_API_KEY,
version: 'weekly',
libraries: ['core', 'maps', 'places', 'geometry', 'geocoding']
// Use .importLibrary('core'); in place of .load() to make google.maps available in the global scope.
}).importLibrary('core');
}
};
Then throughout your application at runtime, you can use google.maps.importLibrary() to make any library available in your local scope. You can directly await and use the constructors or methods rather than accessing them from google.maps.
For example:
try {
// Verify Google Maps JS API is loaded
await LoadGoogleMaps();
// Instantiate Autocomplete Service
const { AutocompleteService } = await google.maps.importLibrary('places');
const $autocomplete = new AutocompleteService();
// Instantiate Geocoding Service
const { Geocoder } = await google.maps.importLibrary('geocoding');
const $geocoder = new Geocoder();
} catch (error) {
console.error(error);
}
This documentation was very helpful: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#js-api-loader, but the key for me was using .importLibrary('core') in place of .load().
Hope this helps anyone else who comes across the "You have included Google Maps API multiple times" warning in the console.
In case it helps somebody, the multiple import issue for me was resolved using Promise.all
const [mapsLibrary, markerLibrary] = await Promise.all([
loader.importLibrary("maps"),
loader.importLibrary("marker"),
]);