React
react-apexmaps is a component around the imperative ApexMaps class. It builds the instance on mount, subscribes every core event, forwards later prop changes to updateOptions() or updateSeries(), and tears the instance down on unmount.
Installation
npm install apexmaps react-apexmaps
apexmaps and react (>=18.0.0) are peer dependencies, so there is one copy of each in your app.
Usage
import ApexMaps from 'react-apexmaps'
import 'apexmaps/apexmaps.css'
const data = [
{ key: 'IND', value: 42 },
{ key: 'BRA', value: 71 },
]
export default function Coverage() {
return (
<ApexMaps
options={{ geo: { map: 'world' } }}
series={[
{ type: 'choropleth', name: 'Coverage', data, scale: { palette: 'blues', classes: 5 } },
]}
onFeatureClick={({ key }) => console.log(key)}
height={480}
/>
)
}
Props
| Prop | Type | Notes |
|---|---|---|
options | ApexMapsOptions | Required. The same options object the core takes. |
series | Series[] | Optional shorthand for options.series. Takes precedence over it; both routes reach updateSeries(), so both tween. |
width / height | number | string | Shorthand for options.chart.width / .height. See Sizing below. |
mapRef | { current: ApexMaps | null } | Receives the live instance, for the imperative API. Nulled on unmount. |
on* | function | One prop per core event, listed below. |
| anything else | Forwarded to the outer <div>: className, style, id, aria-*. |
Events
One prop per core event: onRendered, onUpdated, onResized, onFeatureClick, onFeatureHover, onFeatureFocus, onMarkClick, onMarkHover, onClusterClick, onDrilldown, onDrillup, onSelectionChange, onLegendToggle, onZoom, onPanEnd. Every core event is subscribed internally whether or not you pass a handler, and handlers are read at emit time, so an inline arrow function costs nothing and never misses an event fired between renders.
Payload shapes for each event are documented on API: Methods and Events.
The imperative API
Camera moves, drilldown, export, and diagnostics are methods, not options, because they are actions rather than state. Reach them through mapRef:
const map = useRef(null)
<ApexMaps mapRef={map} options={options} />
<button onClick={() => map.current?.frameFeature('IND')}>Zoom to India</button>
<button onClick={() => map.current?.exportPNG()}>Download</button>
Change detection
React hands the component a brand new options object on every parent render, so it compares deeply rather than by reference. Four rules in that comparison are worth knowing:
Functions compare by source, not identity. An inline formatter is a new function on every render but usually the same behavior, and treating that as a change would make every render a redraw. Editing the formatter's source is noticed; a formatter reading changed state through a closure is not, so lift such values into options where they are data.
geo.map compares by identity and is never walked. Object geometry can be megabytes, and deep-comparing it on every render would cost more than the redraw it avoids. Pass a stable reference: a module-level constant, useMemo, or a registry id string.
Series-only changes go to updateSeries(), which tweens. Everything else goes to updateOptions(), which redraws. This holds whether the data is in the series prop or in options.series.
A map or projection change reprojects; nothing else does. Camera position, selection, and drilldown depth survive an options change, so a parent re-render does not throw away where the reader had navigated to.
One thing to watch
updateOptions() merges, so removing a key from options does not reset it. This is core behavior rather than something the wrapper adds, and it does not fit a fully declarative style:
// Turning `show` off leaves the labels on, because `dataLabels` merely disappears.
options={{ ...base, ...(show && { dataLabels: { enabled: true } }) }}
// Pass the value instead of omitting the key.
options={{ ...base, dataLabels: { enabled: show } }}
Sizing
Use the width and height props, not CSS. The map's height comes from options.chart.height, which defaults to 400; an explicit number wins over the container, so a style={{ height: 600 }} on its own gives you a 600px box with a 400px map inside it.
Width behaves differently, because chart.width defaults to '100%': it follows the container and keeps following it as the container resizes. To have the height follow the container too, say so explicitly:
<ApexMaps options={options} height="100%" style={{ height: '60vh' }} />
Both props accept a number (pixels) or a CSS string; a percentage string hands the dimension to the container.
The DOM it renders
Two nested <div>s. React owns the outer one and applies your className, style, and other attributes to it. ApexMaps owns the inner one and writes its own class and CSS custom properties there. The split matters because React replaces the whole className attribute on change: sharing one element would mean a later className update deletes the apexmaps class and every style rule in the package stops matching.
Server rendering
The component is safe to import and render on the server: it emits its container and does nothing else, because everything that touches a document happens in an effect. It hydrates and then builds the map on the client. It is not server rendered, since the core has no HTML output path yet, so there is no markup to send down ahead of hydration.