Skip to content

Commit 5612ad0

Browse files
NateIsernclaude
andcommitted
Map: simplify review cleanups
- Collapse SHEET_INDEX_DETAIL / SHEET_INDEX_LIST into SHEET_INDEX_MID; rename SHEET_INDEX_SEARCH → SHEET_INDEX_TOP for consistency with the actual snap semantics. - Hoist the static `listContentContainerStyle` out of the component into a module-level `LIST_CONTENT_CONTAINER_STYLE` constant — the empty-dep useMemo was allocation-for-nothing. - Wrap PlaceRow in React.memo so FlashList can skip re-rendering rows that don't touch the current selection change. - Add a change-detection guard to handleSheetChange via the functional setState form so no-op snap repeats don't schedule extra renders. - Trim the stale narrative on handleSearchFocus — the only WHY worth keeping is "no onBlur = avoid focus-blur oscillation". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2f74ecd commit 5612ad0

1 file changed

Lines changed: 25 additions & 39 deletions

File tree

app/map.tsx

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* a `building-3d` extrusion layer that renders 3D buildings at high zoom.
2626
*/
2727

28-
import { useCallback, useMemo, useRef, useState } from "react";
28+
import { memo, useCallback, useMemo, useRef, useState } from "react";
2929
import {
3030
View,
3131
Text,
@@ -135,16 +135,13 @@ const MARKER_HITBOX = { width: 50, height: 50 } as const;
135135
// 62% mid — the Google-Maps-style "selected place" height where the hero
136136
// image, facts, and action row are visible. Index 2 = 100% — the sheet
137137
// fills the screen all the way up behind the notch, sliding under the
138-
// floating search pill. At that snap the list content gains a top
139-
// padding equal to the pill height + safe area so rows never render
140-
// underneath the pill. Magnetic snapping pulls a flick past mid straight
141-
// to the top.
138+
// floating search pill. Magnetic snapping pulls a flick past mid
139+
// straight to the top.
142140
const SHEET_SNAP_POINTS: Array<string | number> = ["15%", "62%", "100%"];
143-
// Detail mode and list mode both use the mid snap so the camera padding
144-
// stays consistent — the only difference is the rendered children.
145-
const SHEET_INDEX_DETAIL = 1;
146-
const SHEET_INDEX_LIST = 1;
147-
const SHEET_INDEX_SEARCH = 2;
141+
const SHEET_INDEX_MID = 1;
142+
const SHEET_INDEX_TOP = 2;
143+
144+
const LIST_CONTENT_CONTAINER_STYLE = { paddingBottom: 24 } as const;
148145

149146
// Fallback map center when PLACES is empty (Madrid).
150147
const FALLBACK_CENTER: Position = [-3.7038, 40.4168];
@@ -324,7 +321,7 @@ export default function MapScreen() {
324321
// Drives the drag-synchronised top spacer inside the list header so the
325322
// first row slides smoothly down as the sheet expands behind the search
326323
// pill, matching Google Maps UX.
327-
const sheetAnimatedIndex = useSharedValue(SHEET_INDEX_LIST);
324+
const sheetAnimatedIndex = useSharedValue(SHEET_INDEX_MID);
328325

329326
// Vertical space occupied by the floating search pill (safe-area top +
330327
// pill top margin + pill height + breathing room). The sheet is free to
@@ -339,22 +336,15 @@ export default function MapScreen() {
339336
PLACES[0]?.id ?? "",
340337
);
341338
const [sheetMode, setSheetMode] = useState<SheetMode>("list");
342-
const [sheetIndex, setSheetIndex] = useState<number>(SHEET_INDEX_LIST);
339+
const [sheetIndex, setSheetIndex] = useState<number>(SHEET_INDEX_MID);
343340
const [hasLocationPermission, setHasLocationPermission] = useState(false);
344341
const [userLocation, setUserLocation] = useState<UserCoords | null>(null);
345342

346-
// Tapping the search field expands the sheet to its top snap so the
347-
// filtered list fills the screen. `KeyboardProvider` at the root
348-
// layout keeps the keyboard in sync with the focused TextInput across
349-
// bottom-sheet snap animations, so we can snap synchronously without
350-
// Android losing focus a beat later.
351-
//
352-
// Intentionally no `onBlur` handler: re-snapping the sheet on blur
353-
// triggers another keyboard lifecycle event which blurs the TextInput
354-
// again, creating a focus-blur oscillation. The user can drag the
355-
// sheet back down themselves if they want.
343+
// No `onBlur` handler: re-snapping on blur triggers another keyboard
344+
// lifecycle event which blurs the TextInput again, creating a
345+
// focus-blur oscillation.
356346
const handleSearchFocus = useCallback(() => {
357-
sheetRef.current?.snapToIndex(SHEET_INDEX_SEARCH);
347+
sheetRef.current?.snapToIndex(SHEET_INDEX_TOP);
358348
}, []);
359349

360350
const handleCategoryFilter = useCallback((next: CategoryFilter) => {
@@ -429,7 +419,7 @@ export default function MapScreen() {
429419
hapticSelection();
430420
setSelectedPlaceId(place.id);
431421
setSheetMode("detail");
432-
sheetRef.current?.snapToIndex(SHEET_INDEX_DETAIL);
422+
sheetRef.current?.snapToIndex(SHEET_INDEX_MID);
433423
if (shouldAnimateCamera) {
434424
animateToCoordinate(place.latitude, place.longitude);
435425
}
@@ -455,26 +445,17 @@ export default function MapScreen() {
455445
}, []);
456446

457447
const handleSheetChange = useCallback((index: number) => {
458-
setSheetIndex(index);
448+
setSheetIndex((prev) => (prev === index ? prev : index));
459449
}, []);
460450

461-
// List content style — constant `paddingBottom` only; the top padding is
462-
// driven per-frame by the animated header spacer below so the transition
463-
// stays synchronised with the user's drag instead of snapping at the
464-
// end of the animation.
465-
const listContentContainerStyle = useMemo(
466-
() => ({ paddingBottom: 24 }),
467-
[],
468-
);
469-
470451
// Animated height for the spacer that sits at the very top of the list
471452
// header. Grows from 0 → sheetTopInset as the sheet's snap index moves
472453
// from mid (1) toward expanded (2), so the first visible row slides
473454
// down in lockstep with the sheet clearing the search pill.
474455
const headerSpacerStyle = useAnimatedStyle(() => ({
475456
height: interpolate(
476457
sheetAnimatedIndex.value,
477-
[SHEET_INDEX_LIST, SHEET_INDEX_SEARCH],
458+
[SHEET_INDEX_MID, SHEET_INDEX_TOP],
478459
[0, sheetTopInset],
479460
Extrapolation.CLAMP,
480461
),
@@ -642,7 +623,7 @@ export default function MapScreen() {
642623
// flush with the pill) reads as grime; a border separates the pill from
643624
// the sheet cleanly.
644625
const pillSurfaceStyle = useMemo(() => {
645-
if (sheetIndex >= SHEET_INDEX_SEARCH) {
626+
if (sheetIndex >= SHEET_INDEX_TOP) {
646627
return {
647628
borderWidth: StyleSheet.hairlineWidth,
648629
borderColor: theme.colors.border,
@@ -813,7 +794,7 @@ export default function MapScreen() {
813794
renderScrollComponent={BottomSheetFlashListScrollable}
814795
data={placesWithDistance}
815796
keyExtractor={(item) => item.place.id}
816-
contentContainerStyle={listContentContainerStyle}
797+
contentContainerStyle={LIST_CONTENT_CONTAINER_STYLE}
817798
ListHeaderComponent={
818799
<View>
819800
<Animated.View style={headerSpacerStyle} />
@@ -939,7 +920,12 @@ interface PlaceRowProps {
939920
onPress: () => void;
940921
}
941922

942-
function PlaceRow({ place, distanceKm: km, isSelected, onPress }: PlaceRowProps) {
923+
const PlaceRow = memo(function PlaceRow({
924+
place,
925+
distanceKm: km,
926+
isSelected,
927+
onPress,
928+
}: PlaceRowProps) {
943929
const theme = useTheme();
944930
const icon = CATEGORY_ICON[place.category];
945931
const distanceLabel = formatDistanceLabel(km);
@@ -1021,7 +1007,7 @@ function PlaceRow({ place, distanceKm: km, isSelected, onPress }: PlaceRowProps)
10211007
</View>
10221008
</Pressable>
10231009
);
1024-
}
1010+
});
10251011

10261012
// ---------------------------------------------------------------------------
10271013
// PlaceDetail — Google Maps inspired place sheet: hero image, title block,

0 commit comments

Comments
 (0)