mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(web): eliminate Leaflet _leaflet_pos crash on geo-map unmount
The Geographic Performance map threw "Cannot read properties of undefined
(reading '_leaflet_pos')" during client-side navigation. An animated
fitBounds/setView schedules a deferred rAF/transitionend callback that reads
the map pane's _leaflet_pos; if the page unmounts before it fires, the pane is
torn down and the callback crashes.
- FitBounds: pass { animate: false } so the camera move applies synchronously
and leaves no dangling animation callback.
- GeoMap: memoize mappableLocations so FitBounds' effect (and the camera move)
only re-runs on real data changes, not on every parent re-render — closing
the amplified race window.
MapCleanup's map.remove() stays as defense-in-depth for instance teardown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { MapContainer, TileLayer, CircleMarker, Tooltip, useMap } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
@@ -81,10 +81,15 @@ function FitBounds({ locations }: { locations: { coords: [number, number] }[] })
|
||||
useEffect(() => {
|
||||
if (locations.length === 0) return;
|
||||
const bounds = locations.map((l) => l.coords) as [number, number][];
|
||||
// animate: false applies the camera move synchronously. An animated move schedules a
|
||||
// deferred requestAnimationFrame/transitionend callback that reads the map pane's
|
||||
// _leaflet_pos; if the component unmounts (client-side navigation) before it fires, the
|
||||
// pane is already torn down and the callback throws "Cannot read properties of undefined
|
||||
// (reading '_leaflet_pos')". A synchronous move leaves no such dangling callback.
|
||||
if (bounds.length === 1) {
|
||||
map.setView(bounds[0], 6);
|
||||
map.setView(bounds[0], 6, { animate: false });
|
||||
} else {
|
||||
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 10 });
|
||||
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 10, animate: false });
|
||||
}
|
||||
}, [locations, map]);
|
||||
|
||||
@@ -92,12 +97,19 @@ function FitBounds({ locations }: { locations: { coords: [number, number] }[] })
|
||||
}
|
||||
|
||||
export default function GeoMap({ locations, globalAvgDuration, selectedLocation, onLocationSelect }: GeoMapProps) {
|
||||
const mappableLocations = locations
|
||||
.map((loc) => {
|
||||
const coords = parseCoords(loc.loc);
|
||||
return coords ? { ...loc, coords } : null;
|
||||
})
|
||||
.filter((l): l is NonNullable<typeof l> => l !== null);
|
||||
// Memoize so the derived array keeps a stable identity across re-renders. FitBounds' effect
|
||||
// depends on this list; a fresh array every render would re-run it (and re-fire the camera
|
||||
// move) on every parent re-render, not just when the location data actually changes.
|
||||
const mappableLocations = useMemo(
|
||||
() =>
|
||||
locations
|
||||
.map((loc) => {
|
||||
const coords = parseCoords(loc.loc);
|
||||
return coords ? { ...loc, coords } : null;
|
||||
})
|
||||
.filter((l): l is NonNullable<typeof l> => l !== null),
|
||||
[locations]
|
||||
);
|
||||
|
||||
if (mappableLocations.length === 0) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user