Skip to content

Commit 03ff0bd

Browse files
committed
fix(map): restore optimistic pin to resolved listing on tap failure
When selectListingById's fetch fails while a previously resolved listing is still visible in the drawer, the pin selection was being cleared to null — leaving the drawer showing listing A with no pin highlighted. Now we capture the currently-resolved listing id (via a ref mirroring selectedListing.id) before the optimistic change, and revert to that id on failure so the pin and the drawer stay in sync. Made-with: Cursor
1 parent 7d20b21 commit 03ff0bd

1 file changed

Lines changed: 23 additions & 5 deletions

File tree

src/features/map/hooks/useMapListingUrl.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation";
55
import { useTranslations } from "next-intl";
66

77
import { createClient } from "@/utils/supabase/client";
8-
import type { Listing, SelectedListing } from "@/types/listing";
8+
import { isListing, type Listing, type SelectedListing } from "@/types/listing";
99
import type { User } from "@supabase/supabase-js";
1010

1111
type UseMapListingUrlArgs = {
@@ -86,6 +86,19 @@ export function useMapListingUrl({
8686
// when slugs flip rapidly or the public/private view changes mid-flight.
8787
const requestTokenRef = useRef(0);
8888

89+
// Id of the listing currently rendered in the drawer (or null when nothing
90+
// or an error is shown). Kept in a ref so `selectListingById` can revert
91+
// the optimistic pin id after a failed tap without going through stale
92+
// closure values.
93+
const resolvedListingIdRef = useRef<number | null>(
94+
initialListing?.id ?? null
95+
);
96+
useEffect(() => {
97+
resolvedListingIdRef.current = isListing(selectedListing)
98+
? (selectedListing.id ?? null)
99+
: null;
100+
}, [selectedListing]);
101+
89102
// If the public/private view flips (e.g. session finished loading) while the
90103
// same listing is open, refetch with the correct view.
91104
useEffect(() => {
@@ -173,6 +186,10 @@ export function useMapListingUrl({
173186

174187
const selectListingById = useCallback(
175188
async (id: number) => {
189+
// Capture the pin id of the drawer's current resolved listing before
190+
// the optimistic change, so we can restore it if the fetch fails.
191+
const previousResolvedId = resolvedListingIdRef.current;
192+
176193
// Tap → pin grows immediately, even before the network round-trip.
177194
setOptimisticListingId(id);
178195
const token = ++requestTokenRef.current;
@@ -190,10 +207,11 @@ export function useMapListingUrl({
190207
// Tap-driven fetches happen before the URL is pushed, so surfacing
191208
// an error sentinel here would either be invisible (no listing in
192209
// URL → drawer stays closed) or desync the UI from the URL (still
193-
// pointing at the previous listing). Revert the optimistic pin and
194-
// leave `selectedListing` alone instead.
210+
// pointing at the previous listing). Revert the optimistic pin to
211+
// whatever the drawer is currently showing and leave
212+
// `selectedListing` alone.
195213
console.warn("Failed to select listing by id:", error);
196-
setOptimisticListingId(null);
214+
setOptimisticListingId(previousResolvedId);
197215
return;
198216
}
199217

@@ -210,7 +228,7 @@ export function useMapListingUrl({
210228
} catch (err) {
211229
if (token !== requestTokenRef.current) return;
212230
console.warn("Failed to select listing by id:", err);
213-
setOptimisticListingId(null);
231+
setOptimisticListingId(previousResolvedId);
214232
}
215233
},
216234
[router, supabase, tableName]

0 commit comments

Comments
 (0)