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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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