Skip to content

[Bug] APIProvider 1.8.x crashes with "google.maps.Settings is undefined" due to race in LOADED fast-path #968

@cramt

Description

@cramt

Description

Since v1.8.0 (PR #913), APIProvider runs an effect that unconditionally calls google.maps.Settings.getInstance() once status === APILoadingStatus.LOADED:

// src/components/api-provider.tsx
useEffect(() => {
  if (status !== APILoadingStatus.LOADED) return;

  const settings = google.maps.Settings.getInstance();
  if (fetchAppCheckToken) {
    settings.fetchAppCheckToken = fetchAppCheckToken;
  } else if (settings.fetchAppCheckToken) {
    settings.fetchAppCheckToken = null;
  }
}, [status, fetchAppCheckToken]);

In the "API already available" fast-path in useGoogleMapsApiLoader, status is set to LOADED before importLibrary('core' / 'maps') has resolved:

if (window.google?.maps?.importLibrary as unknown) {
  if (!serializedApiParams) {
    updateLoadingStatus(APILoadingStatus.LOADED); // <-- fires the Settings effect
  }
  await Promise.all(
    librariesToLoad.map(name => importLibraryCallback(name))
  );
  ...
}

When the Settings effect runs, google.maps.importLibrary exists (the bootstrap installed it) but the actual maps/api/js script has not finished loading yet, so google.maps.Settings is undefined and Settings.getInstance() throws:

TypeError: can't access property "getInstance", google.maps.Settings is undefined

This reproduces reliably in production builds and is mostly invisible in dev, presumably because of scheduling slack and StrictMode double-mount timing differences.

The effect runs regardless of whether fetchAppCheckToken is provided (the effect has no early-return on fetchAppCheckToken === undefined), so there is no userland workaround — omitting the prop does not skip Settings.getInstance().

Suggested fix

Defer the LOADED status update until importLibrary actually resolves in the fast-path, e.g.:

if (window.google?.maps?.importLibrary as unknown) {
  await Promise.all(
    librariesToLoad.map(name => importLibraryCallback(name))
  );
  if (!serializedApiParams) {
    updateLoadingStatus(APILoadingStatus.LOADED);
  }
  if (onLoad) onLoad();
  return;
}

Or, as a defensive fallback, guard the Settings effect:

if (!google.maps.Settings) return;

Steps to Reproduce

  1. Mount <APIProvider apiKey={...}> at the root of a production Vite + React 19 app (SSR or CSR both reproduce).
  2. Don't pass fetchAppCheckToken.
  3. Observe TypeError: can't access property "getInstance", google.maps.Settings is undefined shortly after the provider mounts.

Also reproducible when another component has already caused google.maps.importLibrary to be installed (for example @react-google-maps/api's useJsApiLoader) but the maps script has not finished loading yet.

Environment

  • Library version: @vis.gl/react-google-maps@1.8.0 – 1.8.3 (bisected to PR Add fetchAppCheckToken prop to APIProvider #913)
  • Google maps version: weekly (default, no version prop passed)
  • Browser and Version: Firefox 145, Chrome 144 (any)
  • OS: Linux, macOS

Logs

TypeError: can't access property "getInstance", google.maps.Settings is undefined
    at APIProvider effect (api-provider.tsx:374)

Workaround

Pin @vis.gl/react-google-maps to 1.7.1, which predates PR #913.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions