mapcn-react-native
Data

Clustering

Native point clustering with cluster/count/unclustered-point styling, tap-to-expand, and lazy cluster-leaves queries.

Renderer support: MapLibre ✓  ·  Mapbox ✓ (⚠ minPoints has no equivalent on Mapbox's ShapeSource -- accepted but ignored)

npx mapcn-rn add cluster
import { MapClusterLayer } from "@/components/ui/mapcn";

Overview

MapClusterLayer renders a clustering GeoJSON source plus three layers -- a cluster circle layer, a count-label symbol layer, and an unclustered-point layer -- entirely through native map layers. There is no React Native marker view involved, so this stays correct at 10k+ points; the docs' general guidance is to reach for MapGeoJSON/MapClusterLayer rather than hundreds of MapMarkers.

Props

PropTypeDefaultDescription
idstringauto-generatedSource id.
dataGeoJSONInputRequired. Point features to cluster.
radiusnumber50Cluster radius, in pixels.
maxZoomnumber14Zoom above which points stop clustering.
minPointsnumberMapLibre only. Minimum points to form a cluster. Warns and is ignored on Mapbox.
clusterPropertiesRecord<string, unknown>Native cluster aggregation expressions, e.g. { sum: ["+", ["get", "value"]] }.
cluster{ color?, radius?, steps?: ClusterStep[], strokeColor?, strokeWidth?, opacity? }{ color: "#4285F4", radius: 18, opacity: 0.85 }Cluster-circle styling. steps compiles to a ["step", ["get", "point_count"], ...] expression for size/color that scales with cluster size.
count{ color?, size? } | false{ color: "#ffffff", size: 12 }Count-label styling. false hides the label layer.
pointPointStyle | false{ color: "#4285F4", radius: 5, strokeColor: "#fff", strokeWidth: 1.5 }Unclustered-point styling.
onClusterPress(cluster: Feature, leaves: () => Promise<Feature[]>) => voidFires on cluster tap. leaves() lazily queries the cluster's leaves -- it only runs the (potentially expensive) query if you call it.
onPointPress(feature: Feature, event: MapFeaturePressEvent) => voidFires on an unclustered point tap.
expandOnPressbooleantrueEases the camera to the cluster's expansion zoom on tap, using the source ref's getClusterExpansionZoom (present on both renderers).
beforeIdstringLayer id to insert before.
clusterStyle / countStyle / pointStyleRecord<string, unknown>Raw style escape hatches, merged last.
childrenReactNodeExtra raw layers against the same source.

ClusterStep is { at: number; color: string; radius: number } -- an array of these compiles into the ["step", ...] expression used for cluster.color/cluster.radius when you want cluster appearance to scale with point_count.

Example

import { Map } from "@/components/ui/mapcn";
import { MapClusterLayer } from "@/components/ui/mapcn";

export function ClusteringExample({ points }: { points: GeoJSON.FeatureCollection }) {
  return (
    <Map defaultViewport={{ center: [-98, 39], zoom: 3 }} className="flex-1">
      <MapClusterLayer
        data={points}
        cluster={{
          steps: [
            { at: 0, color: "#4285F4", radius: 16 },
            { at: 100, color: "#F4B400", radius: 22 },
            { at: 750, color: "#DB4437", radius: 28 },
          ],
        }}
        onClusterPress={(cluster, leaves) => {
          leaves().then((features) => console.log(`${features.length} leaves`));
        }}
      />
    </Map>
  );
}

Live example

clustering example screenshot

Renderer support

minPoints (MapLibre's clusterMinPoints) has no Mapbox ShapeSource equivalent. It's accepted on both renderers for a uniform prop type but has no effect on Mapbox -- a tracked, documented capability gap, not a silent drop. getClusterExpansionZoom/getClusterLeaves are normalized across both renderers' native method signatures (MapLibre extracts cluster_id from feature properties internally; Mapbox passes the feature directly) so onClusterPress's leaves() and expandOnPress behave identically either way.

GeoJSON · Heatmaps · Legend

On this page