Clusters
Clustering large datasets on React Native maps.
MapClusterLayer component from the web version is not exposed by mapcn's wrappers. Clustering is still possible in both the MapLibre and Mapbox variants when you drop down to their raw GeoJSON source and layer APIs.GeoJSON Clustering
For clustered datasets, provide a point FeatureCollection through a GeoJSONSource/ShapeSource, turn on source clustering, then render clustered and unclustered features with separate native layers. Upstream MapLibre React Native exposes clustering controls like cluster, clusterRadius, clusterMinPoints, clusterMaxZoomLevel, and clusterProperties, plus helper methods like getClusterExpansionZoom, getClusterLeaves, and getClusterChildren for zooming into or inspecting a cluster after a press event.
mapcn-react-native does not wrap that API today, but you can build it directly with the same low-level GeoJSON layering approach shown in the Advanced Usage GeoJSON layers example and the upstream ShapeSource documentation.
Possible with GeoJSON datasets
- Cluster point features when they are provided through a GeoJSON or shape source.
- Style clustered and unclustered output with native layers such as circle and symbol layers.
- Aggregate cluster metadata with
clusterProperties. - Respond to taps with source
onPressand inspect clusters with the cluster query methods above. - Tap individual unclustered GeoJSON points and read
event.features[0].properties. - Branch between cluster expansion and point selection in the same source.
- Trigger app actions like drawers, bottom sheets, modals, or navigation from tapped point properties.
- Render large point datasets more efficiently than many individual
MapMarkercomponents. - Use GeoJSON for lines and polygons too, as rendered layers rather than clustered markers.
Minimal example
import { Map, useMap } from "@/components/ui/map";
import { GeoJSONSource, Layer } from "@maplibre/maplibre-react-native";
import { useRef, useState } from "react";
import { Pressable, Text, View } from "react-native";
type PlaceFeature = {
type: "Feature";
properties: {
id: string;
title: string;
description: string;
};
geometry: {
type: "Point";
coordinates: [number, number];
};
};
const places = {
type: "FeatureCollection" as const,
features: [
{
type: "Feature" as const,
properties: {
id: "mission",
title: "Mission District",
description: "Tap a point to open a local detail sheet.",
},
geometry: {
type: "Point" as const,
coordinates: [-122.4194, 37.7599] as [number, number],
},
},
{
type: "Feature" as const,
properties: {
id: "downtown",
title: "Downtown",
description: "Use feature properties to drive app UI.",
},
geometry: {
type: "Point" as const,
coordinates: [-122.4058, 37.7897] as [number, number],
},
},
{
type: "Feature" as const,
properties: {
id: "north-beach",
title: "North Beach",
description: "Clusters and single-point actions can share one source.",
},
geometry: {
type: "Point" as const,
coordinates: [-122.4101, 37.8061] as [number, number],
},
},
],
};
type TappableGeoJSONLayerProps = {
onSelectPlace: (place: PlaceFeature["properties"]) => void;
};
function TappableGeoJSONLayer({ onSelectPlace }: TappableGeoJSONLayerProps) {
const { cameraRef } = useMap();
const sourceRef = useRef<any>(null);
return (
<>
<GeoJSONSource
id="places"
data={places}
ref={sourceRef}
cluster
clusterRadius={50}
onPress={async (event) => {
const feature = event.features?.[0];
if (!feature) return;
if (feature.properties?.cluster && cameraRef.current && sourceRef.current) {
const expansionZoom = await sourceRef.current.getClusterExpansionZoom(feature);
cameraRef.current.flyTo({
center: feature.geometry.coordinates as [number, number],
zoom: expansionZoom,
duration: 500,
});
return;
}
onSelectPlace(feature.properties as PlaceFeature["properties"]);
}}
>
<Layer
id="place-clusters"
type="circle"
filter={["has", "point_count"]}
style={{
circleColor: "#2563eb",
circleRadius: 22,
circleOpacity: 0.85,
circleStrokeColor: "#ffffff",
circleStrokeWidth: 2,
}}
/>
<Layer
id="place-points"
type="circle"
filter={["!", ["has", "point_count"]]}
style={{
circleColor: "#f97316",
circleRadius: 10,
circleStrokeColor: "#ffffff",
circleStrokeWidth: 2,
}}
/>
</GeoJSONSource>
</>
);
}
export default function GeoJSONTapMapLibreExample() {
const [selectedPlace, setSelectedPlace] = useState<PlaceFeature["properties"] | null>(null);
return (
<View className="h-[420px] overflow-hidden rounded-xl border border-border relative">
<Map zoom={11} center={[-122.4194, 37.7749]}>
<TappableGeoJSONLayer onSelectPlace={setSelectedPlace} />
</Map>
{selectedPlace && (
<View className="absolute left-4 right-4 bottom-4 rounded-xl border border-border bg-background/95 p-4 shadow-lg">
<Text className="text-base font-semibold text-foreground">
{selectedPlace.title}
</Text>
<Text className="mt-2 text-sm text-muted-foreground">
{selectedPlace.description}
</Text>
<Pressable
onPress={() => setSelectedPlace(null)}
className="mt-4 rounded-lg bg-muted px-3 py-2 items-center"
>
<Text className="font-medium text-foreground">Close</Text>
</Pressable>
</View>
)}
</View>
);
}
Not possible or not provided here
- No first-class
MapClusterLayerabstraction in mapcn's exported API. - No automatic clustering for
MapMarkercomponents. - No clustering for non-point GeoJSON geometries; clustering applies to point shapes.
- No built-in drawer, modal, bottom sheet, or router abstraction for layer-based GeoJSON points.
- No
MarkerPopup,MarkerLabel, or arbitrary React Native marker subtree inside layer-based clusters. - No per-feature React component tree or
MapMarker-style popup behavior for source-rendered points.