# react-map-gl-supercluster

> The easiest way to get `react-map-gl` and `supercluster` to work together

## Highlights

- TypeScript support.
- ESM support.
- Ready for tree shaking.
- No unnecessary re-renders.

## Install

```bash
$ pnpm add react-map-gl-supercluster
# or
$ yarn add react-map-gl-supercluster
# or
$ npm install react-map-gl-supercluster
```

## Usage

Choose the `react-map-gl-supercluster` entrypoint that matches your `react-map-gl` entrypoint.

| `react-map-gl` entrypoint    | `react-map-gl-supercluster` entrypoint    |
| ---------------------------- | ----------------------------------------- |
| `react-map-gl/mapbox`        | `react-map-gl-supercluster/mapbox`        |
| `react-map-gl/maplibre`      | `react-map-gl-supercluster/maplibre`      |
| `react-map-gl/mapbox-legacy` | `react-map-gl-supercluster/mapbox-legacy` |

There is no root entrypoint in v3+ because `react-map-gl` v8 no longer has a default root export.

## Example usage

```tsx
import { type ReactElement, useMemo, useState } from 'react'
import Map, { type MapRef, Marker } from 'react-map-gl/mapbox'
import { isCluster, type PointFeature, useSupercluster } from 'react-map-gl-supercluster/mapbox'

type Item = {
  id: string
  longitude: number
  latitude: number
}
type ItemProperties = { item: Item }
type ItemClusterProperties = { items: Item[] }

function MyAwesomeMap({ items }: { items: Item[] }): ReactElement {
  const [map, setMap] = useState<MapRef | null>(null)

  const points = useMemo(() => createPoints(items), [items])

  const { supercluster, clusters } = useSupercluster(points, {
    mapRef: map,
    map: mapFeature,
    reduce: reduceCluster,
  })

  const expandCluster = (clusterId: number, coordinates: { longitude: number; latitude: number }) => {
    const zoom = supercluster.getClusterExpansionZoom(clusterId)
    map?.easeTo({
      center: [coordinates.longitude, coordinates.latitude],
      zoom,
    })
  }

  return (
    <Map ref={setMap}>
      {clusters.map((cluster) => {
        const [longitude, latitude] = cluster.geometry.coordinates

        return isCluster(cluster) ? (
          <ClusterMarker
            key={`cluster-${cluster.properties.cluster_id}`}
            longitude={longitude}
            latitude={latitude}
            onClick={() => expandCluster(cluster.properties.cluster_id, { longitude, latitude })}
          />
        ) : (
          <Marker key={`item-${cluster.properties.item.id}`} longitude={longitude} latitude={latitude} />
        )
      })}
    </Map>
  )
}

function createPoints(items: Item[]): Array<PointFeature<ItemProperties>> {
  return items.map(createPoint)
}

function createPoint(item: Item): PointFeature<ItemProperties> {
  return {
    type: 'Feature',
    properties: { item },
    geometry: {
      type: 'Point',
      coordinates: [item.longitude, item.latitude],
    },
  }
}

function mapFeature(props: ItemProperties): ItemClusterProperties {
  return { items: [props.item] }
}

function reduceCluster(memo: ItemClusterProperties, props: ItemClusterProperties): void {
  memo.items = memo.items.concat(props.items)
}
```

If the hook is rendered inside `Map`, `mapRef` can be omitted. The hook resolves the current map from the matching `react-map-gl` entrypoint.

```tsx
import { type ReactElement, useMemo } from 'react'
import Map, { Marker } from 'react-map-gl/mapbox'
import { isCluster, useSupercluster } from 'react-map-gl-supercluster/mapbox'

function MyAwesomeMap({ items }: { items: Item[] }): ReactElement {
  return (
    <Map>
      <Markers items={items} />
    </Map>
  )
}

type MarkersProps = {
  items: Item[]
}

function Markers(props: MarkersProps) {
  const { items } = props

  const points = useMemo(() => createPoints(items), [items])

  const { clusters } = useSupercluster(points, {
    map: mapFeature,
    reduce: reduceCluster,
  })

  return (
    <>
      {clusters.map((cluster) => {
        const [longitude, latitude] = cluster.geometry.coordinates

        return isCluster(cluster) ? (
          <ClusterMarker
            key={`cluster-${cluster.properties.cluster_id}`}
            longitude={longitude}
            latitude={latitude}
          />
        ) : (
          <Marker key={`item-${cluster.properties.item.id}`} longitude={longitude} latitude={latitude} />
        )
      })}
    </>
  )
}
```

## Demo

The repository includes a Vite + MapLibre example. It uses the public MapLibre demo style, so no Mapbox token is required.

```bash
$ pnpm example
```

Open the URL printed by Vite.

## API

### `useSupercluster`

The hook can be used in a component which renders `Map` component or inside `Map` children.

#### Arguments

1. `points` - GeoJSON points array. The value should be memoized.
2. `options` – various options, see below.

#### Return value

Object which contains 2 fields:
- `clusters` – clusters list
- `supercluster` – supercluster instance.

### Options

| Option     | Default  | Description                                                                                                                                                                          |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| mapRef     | Optional | `MapRef` instance. Optional when the hook is rendered inside `Map`.                                                                                                                    |
| boundsPadding | 0     | Extra viewport fraction (per side) included when querying clusters. Markers near the edges don't pop in and out, and pans at the same zoom inside the padded area skip recomputation. Larger values render more off-screen markers. |
| minZoom    | 0        | Minimum zoom level at which clusters are generated.                                                                                                                                  |
| maxZoom    | 16       | Maximum zoom level at which clusters are generated.                                                                                                                                  |
| minPoints  | 2        | Minimum number of points to form a cluster.                                                                                                                                          |
| radius     | 40       | Cluster radius, in pixels.                                                                                                                                                           |
| extent     | 512      | Tile extent.                                                                                                                                                                         |
| nodeSize   | 64       | Size of the KD-tree leaf node.                                                                                                                                                       |
| generateId | false    | Whether to generate ids for input features.                                                                                                                                          |
| map        | Optional | A function that returns cluster properties corresponding to a single point. Should be memoized. See `supercluster` property map/reduce options.                                      |
| reduce     | Optional | A reduce function that merges properties of two clusters into one. Should be memoized. See `supercluster` property map/reduce options.                                               |

`react-map-gl-supercluster` supports all `supercluster` options, you can find more information about them [there](https://github.com/mapbox/supercluster#options).

## FAQ

### Why does it cause component re-rendering or why do I get infinite component update loop?

Please be careful with `points` and `map`/`reduce` functions. They always should be memoized.

### Why does TypeScript reject my properties type?

Properties types must satisfy `Record<string, unknown>`. Type aliases get an implicit index signature, `interface` declarations don't — declare properties types with `type`, not `interface`. Also note that the `cluster` key is reserved for generated clusters and must not appear in point properties.

### Does it support WebWorker?

No, the hook is running in the main thread. But probably WebWorker support will come in the future.
