Skip to content

Commit 0b7ad7f

Browse files
TomProkopCopilot
andauthored
Fix infinite update loop crashing the map (React error #185) (#34)
MediaMarkers.tsx was passing an inline arrow function as markerRef, giving it a new identity on every render. useClusterer relies on ref callback identity staying stable per marker (documented in its own comment) - a fresh function each render makes React detach/reattach every AdvancedMarker's ref on every render, which triggers the setMarkers state update in a loop and blows past React's max update depth, crashing the whole page with any geotagged media on the map. Fixed by caching one stable ref-callback function per post id (same pattern useClusterer itself uses internally), reading the post to tag onto the marker from a ref instead of capturing it in a new closure. Verified: npm run build, npm run lint. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent e474251 commit 0b7ad7f

1 file changed

Lines changed: 30 additions & 13 deletions

File tree

src/components/RouteMap/MediaMarkers.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22
import { useMap } from '@vis.gl/react-google-maps';
3+
import type { Marker } from '@googlemaps/markerclusterer';
34
import { useMediaPosts, type MediaPost } from '../../hooks/useMediaPosts';
45
import MediaMarker from './MediaMarker';
56
import MediaLightbox from '../MediaLightbox';
@@ -14,21 +15,37 @@ export default function MediaMarkers() {
1415
const map = useMap();
1516
const setMediaMarkerRef = useClusterer(map, mediaClusterRenderer);
1617

18+
// Keeps the tagging ref callback below stable in identity (see caveat
19+
// on useClusterer: a new function per render causes React to detach/
20+
// reattach every AdvancedMarker's ref on every single render, which
21+
// triggers an infinite setState loop in useClusterer). The post is
22+
// read from this ref instead of being captured directly so the
23+
// wrapper function itself never needs to change.
24+
const postsById = useRef<Record<string, MediaPost>>({});
25+
useEffect(() => {
26+
const next: Record<string, MediaPost> = {};
27+
for (const post of geotagged) next[post.id] = post;
28+
postsById.current = next;
29+
}, [geotagged]);
30+
31+
const taggingRefs = useRef<Record<string, (marker: Marker | null) => void>>({});
32+
function getTaggingRef(id: string) {
33+
if (!taggingRefs.current[id]) {
34+
taggingRefs.current[id] = (marker: Marker | null) => {
35+
// Tag the raw marker instance with its post so the cluster
36+
// renderer (which only receives marker instances, not our
37+
// React props) can pick a thumbnail to show on the bubble.
38+
if (marker) Object.assign(marker, { __mediaPost: postsById.current[id] });
39+
setMediaMarkerRef(id)(marker);
40+
};
41+
}
42+
return taggingRefs.current[id];
43+
}
44+
1745
return (
1846
<>
1947
{geotagged.map((post) => (
20-
<MediaMarker
21-
key={post.id}
22-
post={post}
23-
onClick={() => setSelected(post)}
24-
markerRef={(marker) => {
25-
// Tag the raw marker instance with its post so the cluster
26-
// renderer (which only receives marker instances, not our
27-
// React props) can pick a thumbnail to show on the bubble.
28-
if (marker) Object.assign(marker, { __mediaPost: post });
29-
setMediaMarkerRef(post.id)(marker);
30-
}}
31-
/>
48+
<MediaMarker key={post.id} post={post} onClick={() => setSelected(post)} markerRef={getTaggingRef(post.id)} />
3249
))}
3350
{selected && <MediaLightbox post={selected} onClose={() => setSelected(null)} />}
3451
</>

0 commit comments

Comments
 (0)