Skip to main content

Migrating from use-supercluster

use-supercluster wraps supercluster in a hook: you pass the points, a bbox and a zoom, and it builds the index in JavaScript, on the JS thread. This page is for screens that use it with react-native-maps, and compares use-supercluster 1.2.0 with supercluster 8.0.1.

Pick an entry point​

  • You only need clustered <Marker>s on a react-native-maps map. Use the main MapView instead of react-native-maps' MapView: pass a <Marker> per point as children and delete the hook, the bbox and zoom code, and the cluster markers. It draws a default bubble, or yours with renderCluster, zooms into a cluster on press and spreads out markers that share a location. This is the least code, and what most screens need.
  • You render features yourself, need the GeoJSON features (for a list, or aggregated cluster properties), or use another map component: use useClusterer. The rest of this page is about it.

With MapView:

import MapView from 'react-native-better-clustering'
import { Marker } from 'react-native-maps'

function Map({ places }) {
return (
<MapView initialRegion={initialRegion} style={StyleSheet.absoluteFill}>
{places.map((place) => (
<Marker
key={place.id}
coordinate={{ latitude: place.latitude, longitude: place.longitude }}
/>
))}
</MapView>
)
}

Arguments​

use-superclusteruseClusterer
pointsdata, the first argument: the same GeoJSON point features
bounds and zoomregion and mapDimensions: the map's region and its size in points. The hook computes the bbox and the zoom (see below)
optionsoptions, the fourth argument. radius, minZoom, maxZoom, minPoints, extent and nodeSize mean the same, but minZoom defaults to 1 and maxZoom to 20 here, where supercluster uses 0 and 16: pass both to keep yours. map and reduce become clusterProperties; log and generateId are not supported
disableRefreshNo equivalent. Keep passing the previous data to cluster the previous points in each new region, or the previous data, region and mapDimensions to keep the previous clusters
Returns { clusters, supercluster }Returns [clusters, supercluster]
supercluster is undefined until the first effect has runsupercluster is always an instance; check supercluster.isLoaded before querying it

Cluster features have the same cluster, cluster_id, point_count and point_count_abbreviated properties, plus getExpansionRegion(), a region to animate the map to. getClusterExpansionZoom, getChildren and getLeaves carry over; getTile has no equivalent.

Before and after​

- import useSupercluster from 'use-supercluster'
+ import { useClusterer } from 'react-native-better-clustering/hooks'

function Map({ points }) {
const [region, setRegion] = useState(initialRegion)
- const bounds = [
- region.longitude - region.longitudeDelta / 2,
- region.latitude - region.latitudeDelta / 2,
- region.longitude + region.longitudeDelta / 2,
- region.latitude + region.latitudeDelta / 2,
- ]
- const zoom = Math.log2(360 / region.longitudeDelta)
- const { clusters } = useSupercluster({
- points,
- bounds,
- zoom,
- options: { radius: 40, maxZoom: 16 },
- })
+ const [size, setSize] = useState({ width: 0, height: 0 })
+ const [clusters] = useClusterer(points, size, region, OPTIONS)

return (
<MapView
initialRegion={initialRegion}
+ onLayout={(event) => setSize(event.nativeEvent.layout)}
onRegionChangeComplete={setRegion}
style={StyleSheet.absoluteFill}
>
{clusters.map((feature) => (
<MapFeature key={featureKey(feature)} feature={feature} />
))}
</MapView>
)
}

+ // Module scope or useMemo, so a render never passes a new object.
+ const OPTIONS = { radius: 40, minZoom: 0, maxZoom: 16, viewportTileSize: 128 }

Keep points in useMemo or state. A new array with the same coordinates in the same order, and the same clusterProperties values if you use them, keeps the index, but the hook still compares every point with the previous array. When only other properties change, a point on screen keeps its previous feature object, old properties included, until the region changes, where use-supercluster rebuilds and returns new objects. Read a property that changes from points by a stable id.

Zoom and bbox​

use-supercluster clusters at the zoom you pass, rounded, inside the bbox you pass. useClusterer computes both from region and mapDimensions, the way react-native-clusterer does:

  • The bbox is twice the region's width and height, so it includes points just off screen.
  • The zoom is the highest whole zoom at which that bbox fits the map, counting tiles of viewportTileSize points (default: extent, 512).

With the default, that zoom is usually two levels below Math.log2(360 / longitudeDelta), the zoom of the example above, so radius reaches about four times as far on the map, and each cluster covers about 16 times the area it did. viewportTileSize: 128, as above, brings it to within one level of it on a phone in portrait. Compare the two on your map and adjust radius if needed. See Viewport tile size.

map and reduce​

supercluster's map and reduce run JavaScript for every point and cluster. clusterProperties covers the common case, a sum, minimum or maximum of a numeric property, and runs in C++ while the index builds:

const options = {
radius: 40,
- map: (props) => ({ cost: props.cost, severity: props.severity }),
- reduce: (acc, props) => {
- acc.cost += props.cost
- acc.severity = Math.max(acc.severity, props.severity)
- },
+ clusterProperties: [
+ { source: 'cost', reduce: 'sum' },
+ { source: 'severity', reduce: 'max' },
+ ],
}
// A cluster's properties.cost and properties.severity, as before

Anything else, such as a count per category, has no equivalent: aggregate the cluster's points yourself with supercluster.getLeaves(clusterId, Infinity), for the clusters you render. See SuperclusterOptions.

What runs on which thread​

Stepuse-supercluster 1.2.0 + supercluster 8.0.1useClusterer
Comparing the inputsEvery render: points, bounds, zoom and options are deep-compared with the previous render's; a new points array is compared feature by feature, properties includedEvery render: data is compared with the previous array by identity, then point by point when it is a new array
Building the indexJS thread, in an effectA background thread
When it rebuildsWhen points or options change, and also on every change of bounds or zoom unless options spells out every supercluster option (see below)Only when the points' coordinates, their order or a clusterProperties value changes, or an option changes
Clusters for the regionJS thread, in an effect, then one more renderJS thread, a synchronous native call, during the render
Clusters on the first render[]; after the first effect[]; after the first build has loaded

The rebuild on every pan: before reusing its index, use-supercluster 1.2.0 deep-compares your options with supercluster.options. supercluster 8 keeps its defaults on that object's prototype, so the comparison finds keys your options lacks and fails, and the hook builds a new index for each new bbox or zoom. Rendered in a test with supercluster 8.0.1 and the options of use-supercluster's README, { radius: 75, maxZoom: 20 }, a mount and four pans built five indexes. Passing every supercluster option (minZoom, maxZoom, minPoints, radius, extent, nodeSize, log, generateId, reduce and map) avoids it: the same mount and pans then built one.

Measurements​

From the hook benchmark: both hooks with the same points and options and no map, in a Release build on the iOS Simulator, medians of 3 runs. use-supercluster gets the region's bbox and Math.log2(360 / longitudeDelta) as its zoom.

Library10,000 points50,000100,000
JS thread blocked by one index builduse-supercluster 1.2.0229 ms1,126 ms2,340 ms
useClusterer1.2 ms5 ms11 ms
Mount to first clustersuse-supercluster 1.2.0229 ms1,143 ms2,330 ms
useClusterer15 ms64 ms165 ms
Each panuse-supercluster 1.2.0223 ms1,205 ms2,380 ms
useClusterer0.7 ms0.8 ms0.7 ms
Each data change, until the new indexuse-supercluster 1.2.0226 ms1,202 ms2,338 ms
useClusterer14 ms59 ms152 ms

A re-render with the same props costs both hooks under a millisecond. While a new index builds, useClusterer keeps returning the previous clusters. A simulator runs on the Mac's CPU, faster than a phone: compare the libraries, not the absolute times.