Skip to content

fix(compat): keep marker and cluster keys stable across zoom and data updates - #59

Merged
jkasprzyk17 merged 4 commits into
mainfrom
fix/stable-marker-keys
Oct 5, 2026
Merged

jkasprzyk17 merged 4 commits into
mainfrom
fix/stable-marker-keys

Conversation

@jkasprzyk17

@jkasprzyk17 jkasprzyk17 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Three commits.

  1. Marker keys (Marker keys are replaced by index keys (marker-${index}) #28). Single markers were rendered as cloneElement(child, { key: marker-${index} }), which discarded the key the app gave the Marker. Inserting or removing a marker earlier in the list shifted every later key. React then handed component instances (callouts, tracksViewChanges snapshots, state) to other points, and remounted markers that hadn't changed. Markers now keep the key Children.toArray gives them: the user's key, or the position for unkeyed static children.
  2. Cluster keys. Bubbles were keyed by the engine cluster id. A cluster gets a new id at each zoom where it gains or loses points, and every rebuild renumbers all ids. So each split, merge or data update removed the native marker and added a new one, which blinks, most visibly on Google Maps. The fix:
    • The engine reports a new minLeafId on every node: the smallest point id among its leaves, or the node's own id for a point. A cluster keeps it across zoom levels while it only gains or loses other leaves.
    • Supercluster.getClusterMinLeaf (@internal) resolves it to the loaded feature. It follows the fix(engine): prevent silent cluster-tree corruption from a non-finite coordinate #54 filtering and replaceLoadedFeatures swaps.
    • MapView keys each bubble, default or renderCluster, by that marker's own key. The bubble stays one native marker that moves and relabels.
  3. Redraw. A mounted bubble now changes its count in place. With tracksViewChanges off (the default), Google Maps keeps showing the first bitmap, so ClusterMarker calls the marker's documented redraw() when its count, colours or font change. A moved bubble is a plain coordinate update and doesn't redraw. This also fixes a bubble that never showed its selectedClusterColor on Google.

Fixes #28
Refs #5: a candidate fix. Only a Google Maps run can confirm it (see Notes).

Test plan

  • bun run lint and bun run format:check
  • cd package && bun run typecheck && bun run test:ci: 135 pass
  • cd package && bun run build && bun run verify:pack && bun run verify:exports
  • bun run docs:build
  • C++: cd package/cpp && c++ -std=c++20 -I. ClusterEngineCore.test.cpp -o cluster_test && ./cluster_test, with a new test that minLeafId equals the smallest leaf for every node at every zoom
  • Nitro spec changed: bun run specs re-run. The library pod compiles for the iOS simulator (xcodebuild -scheme react-native-better-clustering, as in native.yml).
  • MapView tests:
  • Supercluster.getClusterMinLeaf unit tests: index alignment after a skipped non-finite point, replaceLoadedFeatures, and a cluster from another instance
  • ClusterMarker tests: redraw() fires on a count or colour change, not on a move, and not while tracksViewChanges is on
  • Example app: not run. On local Xcode 27 the full app fails in expo-modules-jsi (Expo SDK 56 Swift), unrelated to this PR. Android only via CI.
  • Google Maps provider not tested.

Scope

  • Platforms: both (C++ engine and Nitro spec, plus JS)
  • Providers: both. The redraw matters for Google; Apple Maps updates marker views live.

Risk

  • Type change: EngineClusterNode gains a required minLeafId, so code that builds these objects (mocks) needs it. Treat this as a minor release.
  • Visible behaviour change: on split, merge or data update, cluster bubbles now persist and move instead of being removed and re-added. Only bubbles that genuinely appear or disappear fade.
  • redraw() is called only for the default bubbles. Bubbles from renderCluster keep a persisting key too, so the app owns refreshing them if it turns tracksViewChanges off.

Checklist

  • Nitro specs changed? cd package && bun run specs was re-run (package/nitrogen/ stays git-ignored).
  • Public API or user-facing behaviour changed? README.md, docs/docs/troubleshooting.md ("Markers flicker on zoom") and docs/docs/api/types.md (minLeafId) updated.
  • New behaviour is covered by a test.
  • Commits and the PR title follow Conventional Commits.

Notes


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Single markers were rendered as cloneElement(child, { key:
`marker-${index}` }), discarding the key the app gave the Marker.
Inserting or removing a marker earlier in the list shifted every later
key, so React handed component instances (callouts, tracksViewChanges
snapshots, internal state) to other points and remounted markers that
did not change, churning native annotations. react-native-map-clustering
keeps the user's key.

Render the child as Children.toArray returned it: it keeps the user's key
(`.$key`), and an unkeyed static child keeps its position key. React
already warns about markers mapped from an array without a key, so there
is no separate warning.

Refs #28
Cluster bubbles were keyed by the engine's cluster id. A cluster gets a
new id at every zoom level where it gains or loses points, and every
rebuild renumbers all of them, so each split, merge or data update
unmounted the bubble and mounted a new one: the native marker was
removed and re-added, which blinks, most visibly with Google Maps.

The engine now reports minLeafId on every node: the smallest point id
among its leaves, the node's own id for a point. A cluster keeps it
across zoom levels while it only gains or loses other leaves.
Supercluster resolves it to the loaded feature (getClusterMinLeaf,
internal), so it follows the #54 filtering and replaceLoadedFeatures
swaps, and MapView keys each bubble, default or renderCluster, by that
marker's own key. A cluster that splits on zoom, or survives a rebuild
that inserts a marker, keeps one native marker that moves and relabels.

EngineClusterNode gains a required minLeafId field, so code that builds
these objects (mocks) needs it.

Refs #5
Refs #28
Now that a bubble keeps its native marker while its cluster splits or
merges, its count changes on a mounted marker. With tracksViewChanges off
(the default), Google Maps keeps showing the bitmap it first took, so the
bubble would show a stale count; the same already happened to a bubble
whose colour changed through selectedClusterColor.

When the count, colours or font of a mounted bubble change, call the
marker's redraw(), react-native-maps' documented way to refresh a marker
without tracking every view change. A moved bubble is a plain coordinate
update and needs no redraw. Apple Maps updates the view live, and redraw
is harmless there.

Refs #5
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (2)
CONTRIBUTING.md — auto-discovered
AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 929160ea-aa1b-4d59-94e9-25c65d821c78
📥 Commits

Reviewing files that changed from the base of the PR and between 221970c and f2bbcaa.

📒 Files selected for processing (15)
  • README.md
  • docs/docs/api/types.md
  • docs/docs/troubleshooting.md
  • package/cpp/ClusterEngineCore.hpp
  • package/cpp/ClusterEngineCore.test.cpp
  • package/cpp/GeoUtils.hpp
  • package/cpp/HybridClusterEngine.cpp
  • package/src/__tests__/fakes/fakeClusterEngine.ts
  • package/src/__tests__/mapview/MapView.stability.test.tsx
  • package/src/compat/ClusterMarker.test.tsx
  • package/src/compat/ClusterMarker.tsx
  • package/src/compat/MapView.tsx
  • package/src/engine/Supercluster.test.ts
  • package/src/engine/Supercluster.ts
  • package/src/specs/EngineClusterNode.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Markers and cluster bubbles now retain stable identities as markers are added or removed, clusters change during zooming, or the map is rebuilt, reducing flicker and unintended remounts.
    • Cluster bubbles refresh their appearance when their count or styling changes, while avoiding unnecessary redraws for position-only changes.
  • New Features

    • Cluster data now exposes the smallest point ID among its leaves, which can help keep cluster marker keys stable across zoom levels.
    • Added a method to retrieve the point associated with a cluster’s smallest leaf.
  • Documentation

    • Updated marker key guidance and documented the cluster’s smallest-leaf ID.

Walkthrough

The cluster engine now carries the smallest leaf point ID through cluster nodes. MapView uses stable marker keys for unclustered markers and cluster bubbles. ClusterMarker redraws when its rendered appearance changes while native view tracking is disabled.

Changes

Stable marker identity and rendering

Layer / File(s) Summary
Minimum-leaf identity through the engine
package/cpp/GeoUtils.hpp, package/cpp/ClusterEngineCore.hpp, package/cpp/HybridClusterEngine.cpp, package/src/specs/EngineClusterNode.ts, package/src/engine/Supercluster.ts, engine and C++ tests, package/src/__tests__/fakes/fakeClusterEngine.ts, docs/docs/api/types.md
Cluster nodes carry the smallest leaf point ID through the C++ and TypeScript engines. Supercluster.getClusterMinLeaf resolves a cluster to its minimum-leaf point. Tests check minimum-leaf values across zoom levels and feature replacement.
Stable marker and cluster bubble keys
package/src/compat/MapView.tsx, package/src/__tests__/mapview/MapView.stability.test.tsx, README.md, docs/docs/troubleshooting.md
MapView preserves keys on unclustered markers and derives cluster bubble keys from the smallest leaf marker when available. Tests cover marker insertion and removal, cluster changes, and rebuilds. The troubleshooting guidance describes stable marker keys and retains memoization advice.
Cluster marker appearance redraw
package/src/compat/ClusterMarker.tsx, package/src/compat/ClusterMarker.test.tsx
ClusterMarker requests a native redraw when point count, colors, or font family change while tracksViewChanges is disabled. Tests check appearance changes, position-only changes, and enabled tracking.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f2bbc

No actionable merge-blocking issue remains; the change is ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f2bbc

The change is narrowly scoped to marker identity and appearance updates. No new privilege or data-access path was identified, but compatibility between updated JavaScript and existing native app versions has not been confirmed.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The traced new flow affects loaded map features and marker reconciliation within a consuming application. The inspected changes do not introduce an authorization decision or privileged sink; this conclusion is limited to the reviewed identity and redraw paths.

Trust Boundaries and Controls

  • observed — The native-to-JavaScript schema changes, but feature resolution remains instance-owned. A cluster object returned by another instance resolves to undefined rather than selecting that instance's loaded data; dedicated tests cover this ownership boundary and replacement-feature resolution.

Resilience and Maintainability Implications

  • observed — Failed asynchronous loads clear the engine, and destruction clears the engine and loaded features. Missing or stale identity mappings therefore do not resolve features from a different Supercluster instance; rendering retains an explicit fallback for unavailable leaf identity.
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning For #28, Children.toArray preserves marker keys, and the tests cover insertion/removal without remounting other markers. The minLeafId lookup lets cluster bubbles use the smallest leaf marker’s ke… Add a development warning for unkeyed markers that React does not already warn about, and test the warning for static JSX children.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 12 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change and uses the required fix prefix. It exceeds the preferred 50-character length, but remains concise enough to identify the change.
Description check ✅ Passed The description explains the marker and cluster key changes, redraw behavior, scope, and validation. It is directly related to the changeset.
Out of Scope Changes check ✅ Passed The minLeafId engine field, lookup, and key changes implement #28’s stable cluster identity objective. The redraw behavior and tests keep persistent cluster bubbles visually current when their appea…
Security Check ✅ Passed No medium-or-higher vulnerability was introduced. The changed runtime code derives cluster identity from validated point indices and uses those values for React keys; it does not pass them to an injec…
Full details: Linked Issues check

Explanation

For #28, Children.toArray preserves marker keys, and the tests cover insertion/removal without remounting other markers. The minLeafId lookup lets cluster bubbles use the smallest leaf marker’s key, with tests for zoom splits and rebuilds. However, the PR adds no development warning for unkeyed markers. React warns for array-mapped markers, but static JSX children can remain unkeyed without a warning. This leaves #28’s warning acceptance criterion unmet.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 12 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

React Doctor found 9 issues in 3 files · 2 errors & 7 warnings · score 55 / 100 (Critical) · full project

Errors

7 warnings

src/compat/ClusterMarker.tsx

  • ⚠️ L263 Non-component export in component file only-export-components

src/compat/MapView.tsx

  • ⚠️ L256 Large component is hard to read and change no-giant-component
  • ⚠️ L308 Ref initializer runs on every render rerender-lazy-ref-init
  • ⚠️ L424 Data passed to parent via effect no-pass-data-to-parent
  • ⚠️ L424 Parent kept in sync with a callback effect no-prop-callback-in-effect
  • ⚠️ L586 Data passed to parent via effect no-pass-data-to-parent
  • ⚠️ L586 Parent kept in sync with a callback effect no-prop-callback-in-effect

Reviewed by React Doctor for commit f2bbcaa. See inline comments for fixes.

# Conflicts:
#	package/src/__tests__/mapview/MapView.stability.test.tsx
@jkasprzyk17
jkasprzyk17 merged commit 6b92c22 into main Oct 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marker keys are replaced by index keys (marker-${index})

1 participant