1- import React , { useCallback , useEffect , useMemo , useState , useContext } from "react"
1+ import React , { useCallback , useEffect , useMemo , useRef , useState , useContext } from "react"
22import { InfoWindow , useMap } from "@vis.gl/react-google-maps"
33import { MarkerClusterer , SuperClusterAlgorithm } from "@googlemaps/markerclusterer"
44import { AuthContext } from "@bloom-housing/shared-helpers"
5+ import debounce from "lodash/debounce"
56import { MapMarkerData } from "./ListingsMap"
67import { MapMarker } from "./MapMarker"
78import styles from "./ListingsCombined.module.scss"
@@ -84,12 +85,11 @@ const sortMarkers = (unsortedMarkers: MapMarkerData[]) => {
8485 )
8586}
8687
87- let delayTimer
88-
8988export const MapClusterer = ( {
9089 mapMarkers,
9190 infoWindowIndex,
9291 setInfoWindowIndex,
92+ visibleMarkers,
9393 setVisibleMarkers,
9494 setIsLoading,
9595 isFirstBoundsLoad,
@@ -105,29 +105,113 @@ export const MapClusterer = ({
105105
106106 const map = useMap ( )
107107
108- const resetVisibleMarkers = ( ) => {
109- const bounds = map . getBounds ( )
110- const newVisibleMarkers = mapMarkers ?. filter ( ( marker ) => bounds ?. contains ( marker . coordinate ) )
111- // Wait to refetch again until the map has finished fitting bounds
112- if ( isFirstBoundsLoad && isDesktop ) return mapMarkers
108+ // 1) Read latest marker state from refs inside callbacks to avoid stale values
109+ // 2) On map idle, only trigger search when visible marker IDs actually change
110+ // 3) Debounce interactions after initial load to reduce re-search while panning/zooming
111+ // 4) Skip marker re-search during the first desktop cycle
112+
113+ // Keep mutable references to the latest values so debounced callbacks don't use stale values
114+ const mapRef = useRef ( map )
115+ const mapMarkersRef = useRef ( mapMarkers )
116+ const visibleMarkersRef = useRef ( visibleMarkers )
117+ const isFirstBoundsLoadRef = useRef ( isFirstBoundsLoad )
118+ const isDesktopRef = useRef ( isDesktop )
119+
120+ useEffect ( ( ) => {
121+ mapRef . current = map
122+ mapMarkersRef . current = mapMarkers
123+ visibleMarkersRef . current = visibleMarkers
124+ isFirstBoundsLoadRef . current = isFirstBoundsLoad
125+ isDesktopRef . current = isDesktop
126+ } , [ isDesktop , isFirstBoundsLoad , map , mapMarkers , visibleMarkers ] )
127+
128+ // Compare only marker ids to determine whether the visible result set changed - sorting keeps comparison stable
129+ const markerSetSignature = useCallback ( ( markers : MapMarkerData [ ] | null | undefined ) => {
130+ return JSON . stringify ( ( markers ?? [ ] ) . map ( ( marker ) => marker . id ) . sort ( ) )
131+ } , [ ] )
132+
133+ // Compute markers currently inside map bounds using the latest map + markers refs
134+ const getVisibleMarkersInBounds = useCallback ( ( ) => {
135+ const currentMap = mapRef . current
136+ if ( ! currentMap ) return [ ]
137+
138+ const bounds = currentMap . getBounds ( )
139+ const currentMapMarkers = mapMarkersRef . current
140+ return currentMapMarkers ?. filter ( ( marker ) => bounds ?. contains ( marker . coordinate ) ) ?? [ ]
141+ } , [ ] )
142+
143+ // If the visible marker set is unchanged, skip updates.
144+ const hasVisibleMarkerChange = useCallback ( ( ) => {
145+ const nextVisibleMarkers = getVisibleMarkersInBounds ( )
146+ const nextSignature = markerSetSignature ( nextVisibleMarkers )
147+ const currentSignature = markerSetSignature ( visibleMarkersRef . current )
148+
149+ return nextSignature !== currentSignature
150+ } , [ getVisibleMarkersInBounds , markerSetSignature ] )
151+
152+ const resetVisibleMarkers = useCallback ( ( ) => {
153+ const newVisibleMarkers = getVisibleMarkersInBounds ( )
154+ // During first desktop cycle, avoid triggering marker-driven search
155+ if ( isFirstBoundsLoadRef . current && isDesktopRef . current ) return
156+
157+ // When the computed visible set is identical, do not retrigger loading
158+ const nextSignature = markerSetSignature ( newVisibleMarkers )
159+ const currentSignature = markerSetSignature ( visibleMarkersRef . current )
160+ if ( nextSignature === currentSignature ) return
113161
114162 setVisibleMarkers ( newVisibleMarkers )
115- }
163+ } , [ getVisibleMarkersInBounds , markerSetSignature , setVisibleMarkers ] )
116164
117- // Whenever the user's map navigation finishes, on a 1 second delay, reset the currently visible markers on the map to re-search the list
118- map . addListener ( "idle" , ( ) => {
119- setIsLoading ( true )
120- clearTimeout ( delayTimer )
121- delayTimer = setTimeout ( resetVisibleMarkers , isFirstBoundsLoad ? 0 : 800 )
122- } )
165+ const debouncedResetVisibleMarkers = useMemo (
166+ ( ) => debounce ( resetVisibleMarkers , 800 ) ,
167+ [ resetVisibleMarkers ]
168+ )
123169
124- map . addListener ( "click" , ( ) => {
125- setInfoWindowIndex ( null )
126- } )
170+ useEffect ( ( ) => {
171+ if ( ! map ) return
127172
128- map . addListener ( "drag" , ( ) => {
129- setInfoWindowIndex ( null )
130- } )
173+ // After pan/zoom settles, refresh visible markers
174+ // First load runs immediately, then later interactions are debounced
175+ const idleListener = map . addListener ( "idle" , ( ) => {
176+ console . log ( "idle listener" )
177+ if ( isFirstBoundsLoad ) {
178+ resetVisibleMarkers ( )
179+ return
180+ }
181+
182+ // Don't flash loading state for unchanged marker sets
183+ if ( ! hasVisibleMarkerChange ( ) ) {
184+ return
185+ }
186+
187+ setIsLoading ( true )
188+ debouncedResetVisibleMarkers ( )
189+ } )
190+
191+ const clickListener = map . addListener ( "click" , ( ) => {
192+ setInfoWindowIndex ( null )
193+ } )
194+
195+ const dragListener = map . addListener ( "drag" , ( ) => {
196+ setInfoWindowIndex ( null )
197+ } )
198+
199+ return ( ) => {
200+ idleListener . remove ( )
201+ clickListener . remove ( )
202+ dragListener . remove ( )
203+ // Clear pending debounced work when dependencies change or component unmounts
204+ debouncedResetVisibleMarkers . cancel ( )
205+ }
206+ } , [
207+ debouncedResetVisibleMarkers ,
208+ hasVisibleMarkerChange ,
209+ isFirstBoundsLoad ,
210+ map ,
211+ resetVisibleMarkers ,
212+ setInfoWindowIndex ,
213+ setIsLoading ,
214+ ] )
131215
132216 useEffect ( ( ) => {
133217 const oldMarkers = sortMarkers ( currentMapMarkers )
@@ -221,7 +305,8 @@ export const MapClusterer = ({
221305 if ( marker ) {
222306 return { ...markers , [ key ] : marker }
223307 } else {
224- const { [ key ] : _ , ...newMarkers } = markers
308+ const newMarkers = { ...markers }
309+ delete newMarkers [ key ]
225310
226311 return newMarkers
227312 }
0 commit comments