From 715250d50def84507a3389704d51449de73c2d66 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:37:27 -0600 Subject: [PATCH] Add error handling for Google Maps DOM conflicts with React MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Google Maps API and React both manipulate the DOM, which can cause conflicts where Google Maps removes nodes that React still has references to. Add graceful error handling: 1. Remove async flag from Google Maps script - use defer only 2. Add try-catch in marker update useEffect to ignore removeChild errors 3. Add cleanup function to properly tear down markers on unmount 4. Log warnings instead of crashing when DOM conflicts occur This allows the app to continue functioning even when there are minor DOM reconciliation issues between Google Maps and React. 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../stations/components/StationMap.tsx | 44 ++++++++++++++----- .../features/stations/utils/maps-loader.ts | 3 +- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/src/features/stations/components/StationMap.tsx b/frontend/src/features/stations/components/StationMap.tsx index 1ed10e4..f01784d 100644 --- a/frontend/src/features/stations/components/StationMap.tsx +++ b/frontend/src/features/stations/components/StationMap.tsx @@ -82,20 +82,34 @@ export const StationMap: React.FC = ({ }; initMap(); + + // Cleanup: clear markers when component unmounts + return () => { + try { + markers.current.forEach((marker) => marker.setMap(null)); + infoWindows.current.forEach((iw) => iw.close()); + markers.current = []; + infoWindows.current = []; + } catch (err) { + // Silently ignore cleanup errors - they don't affect the user + console.debug('[StationMap] Cleanup error (ignored):', err); + } + }; }, []); // Update markers when stations or saved status changes useEffect(() => { if (!map.current) return; - // Clear old markers and info windows - markers.current.forEach((marker) => marker.setMap(null)); - infoWindows.current.forEach((iw) => iw.close()); - markers.current = []; - infoWindows.current = []; + try { + // Clear old markers and info windows + markers.current.forEach((marker) => marker.setMap(null)); + infoWindows.current.forEach((iw) => iw.close()); + markers.current = []; + infoWindows.current = []; - getGoogleMapsApi(); - let allMarkers: google.maps.Marker[] = []; + getGoogleMapsApi(); + let allMarkers: google.maps.Marker[] = []; // Add station markers stations.forEach((station) => { @@ -132,9 +146,19 @@ export const StationMap: React.FC = ({ allMarkers.push(currentLocationMarker.current); } - // Fit bounds to show all markers - if (allMarkers.length > 0) { - fitBoundsToMarkers(map.current, allMarkers); + // Fit bounds to show all markers + if (allMarkers.length > 0) { + fitBoundsToMarkers(map.current, allMarkers); + } + } catch (error) { + // Ignore DOM errors from Google Maps conflicting with React reconciliation + // This is a known issue when Google Maps removes DOM nodes that React still has references to + if (error instanceof Error && error.message.includes('removeChild')) { + console.warn('[StationMap] Ignoring Google Maps DOM conflict:', error.message); + return; + } + // Re-throw other errors + throw error; } }, [stations, savedPlaceIds, currentLocation, onMarkerClick]); diff --git a/frontend/src/features/stations/utils/maps-loader.ts b/frontend/src/features/stations/utils/maps-loader.ts index 09ab938..937c4d4 100644 --- a/frontend/src/features/stations/utils/maps-loader.ts +++ b/frontend/src/features/stations/utils/maps-loader.ts @@ -51,8 +51,7 @@ export function loadGoogleMaps(): Promise { // Create script tag with callback const script = document.createElement('script'); - script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&loading=async&callback=${callbackName}&libraries=places`; - script.async = true; + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=${callbackName}&libraries=places`; script.defer = true; script.onerror = () => {