From f6d7b45a2c0b41424a15e671f21849bc730d7e89 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Tue, 12 May 2026 11:16:56 -0700 Subject: [PATCH] fix(map): don't crash when remounting with a broken cached map instance When the initial map creation fails (e.g. the Maps JavaScript API didn't load), the cached map instance can return a non-Node from getDiv(). The reuse path then called container.appendChild() with that value and threw. Validate the cached div before reusing the instance, and fall back to creating a fresh map otherwise. Fixes #982 --- src/components/__tests__/map.test.tsx | 36 ++++++++++++++++++++++++++ src/components/map/use-map-instance.ts | 21 ++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/components/__tests__/map.test.tsx b/src/components/__tests__/map.test.tsx index 305be0b0..b621f7ed 100644 --- a/src/components/__tests__/map.test.tsx +++ b/src/components/__tests__/map.test.tsx @@ -209,6 +209,42 @@ describe('map instance caching', () => { "map isn't recreated when unmounting and remounting with regular changed options" ); test.todo('removed options are handled correctly'); + + test("doesn't crash when remounting with a broken cached map instance", async () => { + // simulates the case where the initial map-creation failed (e.g. the Maps + // JavaScript API didn't load correctly), leaving a cached map whose + // getDiv() doesn't return a usable DOM node. Remounting must not throw. + const center = {lat: 53.55, lng: 10.05}; + + const {unmount} = render( + , + {wrapper} + ); + await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument()); + + // make the cached instance return a non-Node from getDiv() + const cachedMap = mockInstances.get(google.maps.Map).at(-1)!; + jest.mocked(cachedMap.getDiv).mockReturnValue(undefined as never); + + unmount(); + createMapSpy.mockReset(); + + expect(() => + render( + , + {wrapper} + ) + ).not.toThrow(); + + await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument()); + // a fresh map instance should have been created instead of reusing the broken one + expect(createMapSpy).toHaveBeenCalled(); + }); }); describe('camera configuration', () => { diff --git a/src/components/map/use-map-instance.ts b/src/components/map/use-map-instance.ts index b2b21e4c..1e221ee0 100644 --- a/src/components/map/use-map-instance.ts +++ b/src/components/map/use-map-instance.ts @@ -138,9 +138,21 @@ export function useMapInstance( let mapDiv: HTMLElement; let map: google.maps.Map; - if (reuseMaps && CachedMapStack.has(cacheKey)) { - map = CachedMapStack.pop(cacheKey) as google.maps.Map; - mapDiv = map.getDiv(); + // a cached map can end up in a broken state (e.g. when the initial + // map-creation failed because the Maps JavaScript API didn't load + // correctly). In that case `getDiv()` doesn't return a usable DOM node, + // so we have to discard the cached instance instead of trying to reuse it. + const cachedMap = + reuseMaps && CachedMapStack.has(cacheKey) + ? (CachedMapStack.pop(cacheKey) as google.maps.Map) + : null; + const cachedMapDiv = cachedMap?.getDiv(); + const reusedMap = + cachedMap && cachedMapDiv instanceof Node ? cachedMap : null; + + if (reusedMap) { + map = reusedMap; + mapDiv = cachedMapDiv as HTMLElement; container.appendChild(mapDiv); map.setOptions(mapOptions); @@ -151,6 +163,9 @@ export function useMapInstance( // the map. setTimeout(() => map.moveCamera({}), 0); } else { + // discard a broken cached instance so it doesn't get pushed back + if (cachedMap) google.maps.event.clearInstanceListeners(cachedMap); + mapDiv = document.createElement('div'); mapDiv.style.height = '100%'; container.appendChild(mapDiv);