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:
- React 18/19 StrictMode (double mount in development)
- 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
- Use React 18 or 19 with StrictMode enabled
- Render a
GoogleMapReact component with onGoogleApiLoaded
- Navigate away (unmount) and navigate back (remount)
- 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
Bug Report:
onGoogleApiLoadednot called on remount —initialized_andgoogleApiLoadedCalled_not reset incomponentWillUnmountDescription
google-map-reactfails to reinitialize the map when the component is unmounted and remounted. This affects:The
onGoogleApiLoadedcallback is never called on the second mount, resulting in a blank map container.Steps to reproduce
GoogleMapReactcomponent withonGoogleApiLoadedonGoogleApiLoadedis not called on the second mountMinimal reproduction
Root cause
Analysis of
dist/index.umd.jsreveals three issues:1.
componentDidMountusessetTimeout(0)to call_initMap2.
_initMapguards withinitialized_— once true, never runs again3.
componentWillUnmountdoes NOT reset these flagsRace condition sequence (StrictMode)
Race condition sequence (SPA navigation — production)
Proposed fix
Reset initialization flags in
componentWillUnmount:This ensures that a fresh mount always goes through the full initialization path.
Workaround
Pass
centerasundefinedon first render, then set it viauseEffect. This forces initialization throughcomponentDidUpdate(which checks!_isCenterDefined(prev) && _isCenterDefined(current)) instead ofcomponentDidMount:How the workaround works
centervaluecomponentDidMountcomponentDidUpdateundefined_isCenterDefined(undefined) = false→ skips_initMap()✅{lat, lng}!_isCenterDefined(prev) && _isCenterDefined(current)→ calls_initMap()✅This bypasses the
setTimeout(0)path incomponentDidMountthat causes the race condition with StrictMode.Neither. It is a
google-map-reactlifecycle bug.Environment
google-map-react: 2.2.x (latest on npm)