UX / lifecycle review
Map Browsing UX
Review
A code-level review of the Pracino map browsing render, query, marker, and fan lifecycle. The central question was whether a reported Ostrava fanout flash is a data/query problem, React marker churn, or native iOS rendering noise.
01Executive summary
The captured flash is real marker churn, but the log does not show a data refetch, dropped points, or a settled-layout collapse. The strongest survivor is the deliberate fanout transition at the grid/zoom boundary, amplified by 45 native fan legs being born and 20 clusters being removed in one burst. The two full-to-mini-to-full pin remounts are the most suspicious visual pop inside that otherwise coherent transition.
The current iOS path is Google Maps/Fabric, not MapKit: map-view.tsx:859-875, apps/mobile/app.config.js:91-103, and apps/mobile/ios/Podfile:42-45. Native redraw behavior should therefore be discussed as Google Maps on iOS.
This is a read-only code and log review. No simulator or device validation was performed.
02Flash report
Ranked mechanisms
- Grid/query-boundary composition change — most plausible. A snapped query can remain stable while the padded bounds cross a layout grid. The layout then reselects candidates under a global budget, so a pan or zoom can replace visible clusters and pins without a server response.
- Fan threshold handoff during zoom or fit — plausible.
fansExpandedis derived from live zoom with hysteresis. A pre-settle frame can render expanded fans, then a settled region can recompute the same points under a different snapped zoom or anchor. - Native Google marker redraw — possible amplifier, not primary cause. Stable React children can still be visually redrawn by the native marker view, especially while 45 custom fan legs mount together.
What flash-repro.txt says
At 19:51:08.566, the layout is still clu=20 fans=0; at 19:51:09.924, it becomes clu=0 fans=20(45) with five pins. The following 45 born leg records and 20 gone records account for the transition, and the React arithmetic balances: +225/-175 is 45 legs plus 20 clusters plus five persistent pins. At 19:51:11.037, the settled frame is still fans=20(45), with only two pins remounting mini-to-full.
That rules against a visible source collapse in this capture: drop=0, point count stays at 180, and there are no fetch or query-key records. It does not rule out a composition flash. The debug logging itself may add simulator delay because each leg is logged separately over roughly 165 ms.
One-line discriminator
Add one timestamped transition log containing the snapped query key, ordered map-point identity hash, fansExpanded, layout mode, and marker keys before and after onRegionChangeComplete. If the query/hash is unchanged but layout mode or keys change, this is local composition churn; if the query/hash changes, investigate the boundary or query lifecycle; if both remain stable while the native view flashes, focus on Google marker redraw.
03Lifecycle map
flowchart LR
A[Camera movement] --> B[Live zoom / viewport state]
B --> C{Fan threshold + hysteresis}
B --> D[Snapped padded query bounds]
D --> E[React Query map points]
E --> F[Supercluster index]
C --> G[Marker layout + budget]
F --> G
G --> H[React marker composition]
H --> I[Google Maps native marker views]
I --> J[Settled region / fit callbacks]
J --> B
The important property is that the camera has both a live path and a settled path. Query data, cluster index, fan mode, candidate budget, and native marker identity do not all update at the same boundary.
04Ranked findings
1. Offscreen candidates can consume the visible marker budget
map-bounds.ts:42-61 creates padded bounds; map-marker-layout.ts:406-430 and 488-500 apply a global 120-item budget before final visibility. Offscreen features can therefore displace on-screen pins or clusters, and grid crossings can look like a flash. Confidence: high. Smallest fix: reserve budget for in-viewport candidates, or add a diagnostic that reports visible versus padded candidates and each rejection reason.
2. Map-point and list queries can disagree during fit
Map points use keepPreviousData (use-map-browsing.ts:323-343), while exported isFetching ignores them (460-466). The browsing screen can fit to placeholder pins (browsing-screen.tsx:315-336) while the map query is changing. Confidence: medium-high. Smallest fix: gate fit on the map-point query’s settled identity; diagnostic: log fit source identity beside the active map query key.
3. Some visible map markers are not tappable
The list fetches pages of 100 (use-map-browsing.ts:98-107), but map layout can render up to 120 (map-marker-layout.ts:4-9, 488-500). The tap handler returns when the item is absent (browsing-screen.tsx:252-260). Confidence: high. Smallest fix: make marker composition and interaction use the same loaded-item contract, or log rendered-but-unresolved marker IDs.
4. Large clusters are represented by only 20 leaves
Coincidence, fan placement, and fit use the first 20 leaves (map-marker-layout.ts:22-24, 412-430, 451-472; map-view.tsx:384-447). A cluster with a larger point_count can therefore have misleading geometry. Confidence: high. Smallest fix: explicitly label the approximation and add a diagnostic comparing point_count to sampled leaves.
5. Numeric cluster IDs can preserve stale iOS marker state
The index is rebuilt (map-view.tsx:296-321), but cluster keys use only clusterId (564-583). iOS cluster views are memoized/frozen (map-markers.tsx:268-301, 49-59). If a new index reuses an ID, the count can remain stale. Confidence: medium. Smallest fix: key clusters by index generation plus ID; diagnostic: log generation, cluster ID, count, and rendered native key.
6. Structural point reuse ignores coordinates and order
use-map-browsing.ts:481-499 reuses the previous array based on shallow structure, not an identity-and-coordinate hash. Those coordinates feed indexing and fitting (map-view.tsx:305-318, 731-781, 811-839). Confidence: medium. Smallest fix: include stable ID and lat/lng in the reuse test; diagnostic: log reuse decision with coordinate hash.
7. Optimistic fit region and native fit region differ
runFit computes a raw bbox then applies asymmetric padding (map-view.tsx:739-781), while the native settle callback recomputes cluster viewport (268-293). This can create a brief composition mismatch. Confidence: medium. Smallest fix: record both regions and compare them before changing behavior.
8. Fan hysteresis can disagree with settled zoom
Live zoom and settled zoom use hysteresis (map-view.tsx:111-114, 204-227, 268-282), while layout reads the expanded state (map-marker-layout.ts:401-405). A threshold crossing can therefore show fans for one frame and clusters for the next. Confidence: medium. Smallest fix: log live zoom, settled zoom, and threshold state on every mode change.
9. Selected fan members are not protected by the fan budget
Fan candidates are selected before the budget (map-marker-layout.ts:412-430); the selected rescue only covers standalone pins (488-499). A selected fan member can disappear and dismiss its card (map-view.tsx:516-522, 722-726). Confidence: medium-high. Smallest fix: reserve the selected ID in fan allocation; diagnostic: log selected ID and final layout membership.
10. Fan columns can exceed usable screen width
The fan step is 8.5% of the full window and up to 20 legs are allowed (map-marker-layout.ts:17-24, 120-134), reaching 161.5% before pill height and safe-area constraints. Coincident taps also omit bottom-sheet padding that spread fits apply (map-view.tsx:395-447). Confidence: high. Smallest fix: clamp fan spread to usable width and log clamped layouts.
05Don't touch without evidence
The snapped/padded query and anchor reduce fetch churn during camera movement. Do not put raw viewport bounds back into the point query just to make the lifecycle look simpler.
Do not casually rearm tracksViewChanges, drop marker keys, or remount all markers. First prove that React layout and keys stay stable while the native view flashes.
Render/count/fit sheet-band responsibilities, the synchronous cluster region, and programmatic-fit guards each look unusual but prevent different classes of feedback loop. Change them only with transition logs or a reproduction.
06Evidence and review boundary
- Source passInspected the requested browsing lifecycle and the iOS provider configuration.
- History passChecked recent changes touching the map files for intent and likely regression seams.
- Log passReconciled layout counters, 45 fan births, 20 cluster removals, and the two pin remount pairs.
- Validation boundaryThis artifact intentionally contains no simulator or native runtime confirmation.