Skip to content

onGoogleApiLoaded not called on remount — initialized_ and googleApiLoadedCalled_ not reset in componentWillUnmount #1255

Description

@Albertosnp

Bug Report: onGoogleApiLoaded not called on remount — initialized_ and googleApiLoadedCalled_ not reset in componentWillUnmount

Description

google-map-react fails to reinitialize the map when the component is unmounted and remounted. This affects:

  1. React 18/19 StrictMode (double mount in development)
  2. SPA navigation (unmount/remount in production)

The onGoogleApiLoaded callback is never called on the second mount, resulting in a blank map container.

Steps to reproduce

  1. Use React 18 or 19 with StrictMode enabled
  2. Render a GoogleMapReact component with onGoogleApiLoaded
  3. Navigate away (unmount) and navigate back (remount)
  4. Observe that onGoogleApiLoaded is not called on the second mount

Minimal reproduction

import GoogleMapReact from "google-map-react";
import { useState } from "react";

function App() {
  const [showMap, setShowMap] = useState(true);

  return (
    <div>
      <button onClick={() => setShowMap((v) => !v)}>Toggle Map</button>
      {showMap && (
        <div style={{ width: 400, height: 400 }}>
          <GoogleMapReact
            bootstrapURLKeys={{ key: "YOUR_API_KEY" }}
            defaultCenter={{ lat: 40.416775, lng: -3.70379 }}
            defaultZoom={5}
            onGoogleApiLoaded={({ map, maps }) => {
              console.log("API loaded", map, maps); // ❌ Never called on 2nd mount
            }}
            yesIWantToUseGoogleMapApiInternals
          />
        </div>
      )}
    </div>
  );
}

Root cause

Analysis of dist/index.umd.js reveals three issues:

1. componentDidMount uses setTimeout(0) to call _initMap

// Deobfuscated from dist/index.umd.js
componentDidMount() {
  this.mounted_ = true;
  // ...
  setTimeout(function() {
    e._setViewSize();
    e._isCenterDefined(e.props.center || e.props.defaultCenter) && e._initMap();
  }, 0);
}

2. _initMap guards with initialized_ — once true, never runs again

_initMap = function() {
  if (!n.initialized_) {       // After 1st call, blocks forever
    n.initialized_ = true;
    // ...
    n.props.googleMapLoader(t).then(function(e) {
      if (n.mounted_) {        // May be false due to StrictMode unmount
        // new e.Map(...)
        // onGoogleApiLoaded(...)
        y.googleApiLoadedCalled_ = true;
      }
    });
  }
}

3. componentWillUnmount does NOT reset these flags

componentWillUnmount() {
  this.mounted_ = false;
  // ❌ Missing: this.initialized_ = false;
  // ❌ Missing: this.googleApiLoadedCalled_ = false;
  // ...rest of cleanup...
}

Race condition sequence (StrictMode)

1. mount()      → mounted_ = true,  setTimeout(0) queued
2. unmount()    → mounted_ = false                          ← StrictMode
3. mount()      → mounted_ = true,  setTimeout(0) queued
4. 1st setTimeout fires → _initMap()
   → initialized_ = true
   → loader.then() → checks mounted_ (may reference stale closure)
   → googleApiLoadedCalled_ = true
5. 2nd setTimeout fires → _initMap()
   → initialized_ === true → SKIPPED ❌
   → Map never created for current DOM

Race condition sequence (SPA navigation — production)

1. mount()      → map created, initialized_ = true, googleApiLoadedCalled_ = true
2. unmount()    → mounted_ = false (initialized_ and googleApiLoadedCalled_ NOT reset)
3. mount()      → setTimeout → _initMap() → initialized_ === true → SKIPPED ❌

Proposed fix

Reset initialization flags in componentWillUnmount:

componentWillUnmount() {
  this.mounted_ = false;
  this.initialized_ = false;           // ← ADD
  this.googleApiLoadedCalled_ = false;  // ← ADD
  // ...rest of existing cleanup...
}

This ensures that a fresh mount always goes through the full initialization path.

Workaround

Pass center as undefined on first render, then set it via useEffect. This forces initialization through componentDidUpdate (which checks !_isCenterDefined(prev) && _isCenterDefined(current)) instead of componentDidMount:

const [deferredCenter, setDeferredCenter] = useState<MapCenter | undefined>(undefined);

useEffect(() => {
  setDeferredCenter(center);
}, [center]);

<GoogleMapReact center={deferredCenter} ... />

How the workaround works

Render center value componentDidMount componentDidUpdate
1st undefined _isCenterDefined(undefined) = false → skips _initMap()
2nd {lat, lng} !_isCenterDefined(prev) && _isCenterDefined(current) → calls _initMap()

This bypasses the setTimeout(0) path in componentDidMount that causes the race condition with StrictMode.

Neither. It is a google-map-react lifecycle bug.

Scenario StrictMode ON StrictMode OFF
Dev — first render ❌ Bug visible ✅ OK
Dev — SPA navigation (unmount/remount) ❌ Bug visible Bug visible
Prod — first render ✅ OK ✅ OK
Prod — SPA navigation (unmount/remount) N/A Bug visible

Environment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions