Skip to content

Commit 541fcb0

Browse files
refactor: switch to debounce to avoid high idle calls
1 parent 74c593f commit 541fcb0

File tree

5 files changed

+119
-22
lines changed

5 files changed

+119
-22
lines changed

sites/public/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"clone-deep": "^4.0.1",
4545
"dayjs": "^1.10.7",
4646
"dotenv": "^17.2.3",
47+
"lodash": "^4.17.23",
4748
"mapbox-gl": "^3.16.0",
4849
"markdown-to-jsx": "^7.7.16",
4950
"next": "^15.5.7",

sites/public/src/components/browse/map/ListingsList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const ListingsList = (props: ListingsListProps) => {
4141
</div>
4242
)
4343

44+
console.log(props.loading)
45+
4446
const infoCards = (
4547
<div className={styles["info-cards-container"]}>
4648
{process.env.notificationsSignUpUrl && (

sites/public/src/components/browse/map/ListingsSearchCombined.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ function ListingsSearchCombined(props: ListingsSearchCombinedProps) {
142142
(isDesktop || listView) &&
143143
!(visibleMarkers?.length === 0 && changingFilter))
144144
) {
145+
console.log("pre search loading true")
145146
setIsLoading(true)
146147
const result = await searchListings(
147148
isDesktop ? listingIdsOnlyQb : genericQb,
@@ -255,6 +256,7 @@ function ListingsSearchCombined(props: ListingsSearchCombinedProps) {
255256

256257
// Re-search when the map's visible markers are changed
257258
useEffect(() => {
259+
console.log("visible markers changed use effect")
258260
async function searchListings() {
259261
await search(1)
260262
}
@@ -268,9 +270,11 @@ function ListingsSearchCombined(props: ListingsSearchCombinedProps) {
268270

269271
// Only re-search if the visible markers are not the same
270272
if (oldMarkers !== newMarkers && isDesktop) {
273+
console.log("old not equal to new")
271274
setCurrentMarkers(visibleMarkers)
272275
void searchListings()
273276
} else {
277+
console.log("set loading, to false they are the same")
274278
setIsLoading(false)
275279
}
276280
// eslint-disable-next-line react-hooks/exhaustive-deps

sites/public/src/components/browse/map/MapClusterer.tsx

Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React, { useCallback, useEffect, useMemo, useState, useContext } from "react"
1+
import React, { useCallback, useEffect, useMemo, useRef, useState, useContext } from "react"
22
import { InfoWindow, useMap } from "@vis.gl/react-google-maps"
33
import { MarkerClusterer, SuperClusterAlgorithm } from "@googlemaps/markerclusterer"
44
import { AuthContext } from "@bloom-housing/shared-helpers"
5+
import debounce from "lodash/debounce"
56
import { MapMarkerData } from "./ListingsMap"
67
import { MapMarker } from "./MapMarker"
78
import styles from "./ListingsCombined.module.scss"
@@ -84,12 +85,11 @@ const sortMarkers = (unsortedMarkers: MapMarkerData[]) => {
8485
)
8586
}
8687

87-
let delayTimer
88-
8988
export 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
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8598,6 +8598,11 @@ lodash@4.17.21, lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, l
85988598
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
85998599
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
86008600

8601+
lodash@^4.17.23:
8602+
version "4.17.23"
8603+
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
8604+
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
8605+
86018606
log-symbols@^4.0.0, log-symbols@^4.1.0:
86028607
version "4.1.0"
86038608
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz"

0 commit comments

Comments
 (0)