diff --git a/docs/api-reference/map-provider.md b/docs/api-reference/map-provider.md
new file mode 100644
index 00000000..cf10f012
--- /dev/null
+++ b/docs/api-reference/map-provider.md
@@ -0,0 +1,21 @@
+# MapProvider
+
+A [Context.Provider](https://reactjs.org/docs/context.html#contextprovider) that facilitates map operations outside of the component that directly renders a [Map](/docs/api-reference/map.md).
+
+The component should wrap all nodes in which you may want to access the maps:
+
+```js
+import {MapProvider} from 'react-map-gl';
+
+function Root() {
+ return (
+
+ {
+ // Application tree, somewhere one or more component(s) are rendered
+ }
+
+ );
+}
+```
+
+See [useMap](/docs/api-reference/use-map.md) for more information.
diff --git a/docs/api-reference/map.md b/docs/api-reference/map.md
index fec6f4fb..4b7b6456 100644
--- a/docs/api-reference/map.md
+++ b/docs/api-reference/map.md
@@ -24,11 +24,8 @@ function App() {
## Methods
-#### `getMap()`
+The following methods are accessible via a [React ref](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) or the [useMap](/docs/api-reference/use-map.md) hook.
-Returns the underlying [Map](https://docs.mapbox.com/mapbox-gl-js/api/map/) instance.
-
-Example:
```js
import * as React from 'react';
@@ -46,6 +43,13 @@ function App() {
}
```
+#### `getMap()`
+
+Returns the underlying [Map](https://docs.mapbox.com/mapbox-gl-js/api/map/) instance.
+
+#### `getViewState()`
+
+Returns the current view state of the map.
## Properties
diff --git a/docs/api-reference/use-control.md b/docs/api-reference/use-control.md
new file mode 100644
index 00000000..af053046
--- /dev/null
+++ b/docs/api-reference/use-control.md
@@ -0,0 +1,64 @@
+# useControl
+
+The `useControl` hook is used to create React wrappers for custom map controls.
+
+```js
+import MapboxDraw from '@mapbox/mapbox-gl-draw';
+import {Map, useControl} from 'react-map-gl';
+
+function DrawControl(props: DrawControlProps) {
+ useControl(() => new MapboxDraw(props), {
+ position: props.position
+ });
+
+ return null;
+}
+
+function App() {
+ return (
+
+ );
+}
+```
+
+See a full example [here](/examples/draw-polygon).
+
+## Signature
+
+```js
+useControl(onCreate: () => IControl, options?: {
+ position?: ControlPosition;
+ onAdd?: (map: MapboxMap) => void;
+ onRemove?: (map: MapboxMap) => void;
+}): IControl
+```
+
+The hook creates an [IControl](https://docs.mapbox.com/mapbox-gl-js/api/markers/#icontrol) instance, adds it to the map when it's available, and removes it upon unmount.
+
+Parameters:
+
+- `onCreate` (Function) - called to create an instance of the control.
+- `options` (object)
+ + `position` ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right') - control position relative to the map
+ + `onAdd` (Function) - called after the control is added to a map
+ + `onRemove` (Function) - called before the control is removed from a map
+
+Returns:
+
+The control instance.
diff --git a/docs/api-reference/use-map.md b/docs/api-reference/use-map.md
new file mode 100644
index 00000000..0ca1f3ae
--- /dev/null
+++ b/docs/api-reference/use-map.md
@@ -0,0 +1,37 @@
+# useMap
+
+The `useMap` hook, used with the [MapProvider](/docs/api-reference/map-provider.md), helps an app to perform map operations outside of the component that directly renders a [Map](/docs/api-reference/map.md).
+
+
+```js
+import {MapProvider, Map, useMap} from 'react-map-gl';
+
+function Root() {
+ return (
+
+
+
+
+
+ );
+}
+
+function NavigateButton() {
+ const {myMapA, myMapB} = useMap();
+
+ const onClick = () => {
+ myMapA.flyTo({center: [-122.4, 37.8]});
+ myMapB.flyTo({center: [-74, 40.7]});
+ };
+
+ return ;
+}
+```
+
+See a full example [here](/examples/get-started/hook).
+
+## Signature
+
+`useMap(): {[id: string]: MapRef}`
+
+The hook returns an object that contains all mounted maps under the closest `MapProvider`. The keys are each map's [id](/docs/api-reference/map.md#prperties) and the values are the [ref object](/docs/api-reference/map.md#methods).
diff --git a/examples/clusters/src/app.tsx b/examples/clusters/src/app.tsx
index aa1368c3..61a3d1cd 100644
--- a/examples/clusters/src/app.tsx
+++ b/examples/clusters/src/app.tsx
@@ -18,15 +18,14 @@ export default function App() {
const feature = event.features[0];
const clusterId = feature.properties.cluster_id;
- const map = mapRef.current.getMap();
- const mapboxSource = map.getSource('earthquakes') as GeoJSONSource;
+ const mapboxSource = mapRef.current.getSource('earthquakes') as GeoJSONSource;
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) {
return;
}
- map.easeTo({
+ mapRef.current.easeTo({
center: feature.geometry.coordinates,
zoom,
duration: 500
diff --git a/examples/draw-polygon/README.md b/examples/draw-polygon/README.md
index 057fe1ab..afa42cab 100644
--- a/examples/draw-polygon/README.md
+++ b/examples/draw-polygon/README.md
@@ -1,6 +1,6 @@
# Example: Draw Polygon
-Demonstrates how to use [react-map-gl-draw](https://github.com/uber/nebula.gl/tree/master/modules/react-map-gl-draw) to draw polygons with react-map-gl.
+This app reproduces Mapbox's [Draw a polygon and calculate its area](https://docs.mapbox.com/mapbox-gl-js/example/mapbox-gl-draw/) example.
## Usage
diff --git a/examples/draw-polygon/app.css b/examples/draw-polygon/app.css
deleted file mode 100644
index 72db167a..00000000
--- a/examples/draw-polygon/app.css
+++ /dev/null
@@ -1,24 +0,0 @@
-body {
- margin: 0;
- background: #000;
-}
-#map {
- width: 100vw;
- height: 100vh;
-}
-
-.control-panel {
- position: absolute;
- top: 0;
- right: 0;
- max-width: 320px;
- background: #fff;
- box-shadow: 0 2px 4px rgba(0,0,0,0.3);
- padding: 12px 24px;
- margin: 20px;
- font-size: 13px;
- line-height: 2;
- color: #6b6b76;
- text-transform: uppercase;
- outline: none;
-}
diff --git a/examples/draw-polygon/index.html b/examples/draw-polygon/index.html
index 3506f5a3..a9e93176 100644
--- a/examples/draw-polygon/index.html
+++ b/examples/draw-polygon/index.html
@@ -1,17 +1,41 @@
-
-
Draw Polygon
- {polygon && (
+ {polygonArea > 0 && (
- {polygonArea}
+ {Math.round(polygonArea * 100) / 100}
square meters
)}
diff --git a/examples/draw-polygon/src/draw-control.ts b/examples/draw-polygon/src/draw-control.ts
new file mode 100644
index 00000000..1775061c
--- /dev/null
+++ b/examples/draw-polygon/src/draw-control.ts
@@ -0,0 +1,56 @@
+import MapboxDraw from '@mapbox/mapbox-gl-draw';
+import {useControl} from 'react-map-gl';
+
+import type {MapboxMap, ControlPosition} from 'react-map-gl';
+
+type ControlTypes =
+ | 'point'
+ | 'line_string'
+ | 'polygon'
+ | 'trash'
+ | 'combine_features'
+ | 'uncombine_features';
+
+type DrawControlProps = {
+ keybindings?: boolean;
+ touchEnable?: boolean;
+ boxSelect?: boolean;
+ clickBuffer?: number;
+ touchBuffer?: number;
+ controls?: Partial<{[name in ControlTypes]: boolean}>;
+ displayControlsDefault?: boolean;
+ styles?: any;
+ modes?: any;
+ defaultMode?: string;
+ userProperties?: boolean;
+
+ position?: ControlPosition;
+
+ onCreate?: (evt: {features: object[]}) => void;
+ onUpdate?: (evt: {features: object[]; action: string}) => void;
+ onDelete?: (evt: {features: object[]}) => void;
+};
+
+export default function DrawControl(props: DrawControlProps) {
+ useControl(() => new MapboxDraw(props), {
+ position: props.position,
+ onAdd: (map: MapboxMap) => {
+ map.on('draw.create', props.onCreate);
+ map.on('draw.update', props.onUpdate);
+ map.on('draw.delete', props.onDelete);
+ },
+ onRemove: (map: MapboxMap) => {
+ map.off('draw.create', props.onCreate);
+ map.off('draw.update', props.onUpdate);
+ map.off('draw.delete', props.onDelete);
+ }
+ });
+
+ return null;
+}
+
+DrawControl.defaultProps = {
+ onCreate: () => {},
+ onUpdate: () => {},
+ onDelete: () => {}
+};
diff --git a/examples/draw-polygon/src/style.js b/examples/draw-polygon/src/style.js
deleted file mode 100644
index bfcfd859..00000000
--- a/examples/draw-polygon/src/style.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {RENDER_STATE} from 'react-map-gl-draw';
-
-export function getEditHandleStyle({feature, state}) {
- switch (state) {
- case RENDER_STATE.SELECTED:
- case RENDER_STATE.HOVERED:
- case RENDER_STATE.UNCOMMITTED:
- return {
- fill: 'rgb(251, 176, 59)',
- fillOpacity: 1,
- stroke: 'rgb(255, 255, 255)',
- strokeWidth: 2,
- r: 7
- };
-
- default:
- return {
- fill: 'rgb(251, 176, 59)',
- fillOpacity: 1,
- stroke: 'rgb(255, 255, 255)',
- strokeWidth: 2,
- r: 5
- };
- }
-}
-
-export function getFeatureStyle({feature, index, state}) {
- switch (state) {
- case RENDER_STATE.SELECTED:
- case RENDER_STATE.HOVERED:
- case RENDER_STATE.UNCOMMITTED:
- case RENDER_STATE.CLOSING:
- return {
- stroke: 'rgb(251, 176, 59)',
- strokeWidth: 2,
- fill: 'rgb(251, 176, 59)',
- fillOpacity: 0.3,
- strokeDasharray: '4,2'
- };
-
- default:
- return {
- stroke: 'rgb(60, 178, 208)',
- strokeWidth: 2,
- fill: 'rgb(60, 178, 208)',
- fillOpacity: 0.1
- };
- }
-}
diff --git a/examples/draw-polygon/tsconfig.json b/examples/draw-polygon/tsconfig.json
new file mode 100644
index 00000000..ff49de04
--- /dev/null
+++ b/examples/draw-polygon/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "jsx": "react",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "node",
+ "sourceMap": true
+ }
+}
\ No newline at end of file
diff --git a/examples/draw-polygon/webpack.config.js b/examples/draw-polygon/webpack.config.js
index 78fe2a6a..ed4df0d5 100644
--- a/examples/draw-polygon/webpack.config.js
+++ b/examples/draw-polygon/webpack.config.js
@@ -5,33 +5,40 @@
const resolve = require('path').resolve;
const webpack = require('webpack');
-const BABEL_CONFIG = {
- presets: ['@babel/env', '@babel/react'],
- plugins: ['@babel/proposal-class-properties']
-};
-
const config = {
mode: 'development',
+ devServer: {
+ static: '.'
+ },
+
entry: {
- app: resolve('./src/app.js')
+ app: resolve('./src/app')
},
output: {
library: 'App'
},
+ resolve: {
+ extensions: ['.ts', '.tsx', '.js', '.json']
+ },
+
module: {
rules: [
{
- // Compile ES2015 using babel
- test: /\.js$/,
+ test: /\.(ts|js)x?$/,
include: [resolve('.')],
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
- options: BABEL_CONFIG
+ options: {
+ presets: ['@babel/env', '@babel/react']
+ }
+ },
+ {
+ loader: 'ts-loader'
}
]
}
@@ -39,7 +46,7 @@ const config = {
},
// Optional: Enables reading mapbox token from environment variable
- plugins: [new webpack.EnvironmentPlugin(['MapboxAccessToken'])]
+ plugins: [new webpack.EnvironmentPlugin({MapboxAccessToken: ''})]
};
// Enables bundling against src in this repo rather than the installed version
diff --git a/examples/geojson/src/app.tsx b/examples/geojson/src/app.tsx
index dfceeb29..4a08882f 100644
--- a/examples/geojson/src/app.tsx
+++ b/examples/geojson/src/app.tsx
@@ -30,15 +30,8 @@ export default function App() {
} = event;
const hoveredFeature = features && features[0];
- setHoverInfo(
- hoveredFeature
- ? {
- feature: hoveredFeature,
- x,
- y
- }
- : null
- );
+ // prettier-ignore
+ setHoverInfo(hoveredFeature && {feature: hoveredFeature, x, y});
}, []);
const data = useMemo(() => {
diff --git a/examples/get-started/hook/README.md b/examples/get-started/hook/README.md
new file mode 100644
index 00000000..6a2e74e9
--- /dev/null
+++ b/examples/get-started/hook/README.md
@@ -0,0 +1,18 @@
+# react-map-gl Example: Using Map with a State Management System
+
+This example shows how to use react-map-gl's Map component with the `useMap` hook.
+
+## Usage
+
+To run this example, you need a [Mapbox token](http://visgl.github.io/react-map-gl/docs/get-started/mapbox-tokens). You can either set it as `MAPBOX_TOKEN` in `map.js`, or set a `MapboxAccessToken` environment variable in the command line.
+
+```bash
+npm i
+npm run start
+```
+
+To build a production version:
+
+```bash
+npm run build
+```
diff --git a/examples/get-started/hook/app.js b/examples/get-started/hook/app.js
new file mode 100644
index 00000000..f93eed5a
--- /dev/null
+++ b/examples/get-started/hook/app.js
@@ -0,0 +1,18 @@
+/* global document */
+import * as React from 'react';
+import {render} from 'react-dom';
+import {MapProvider} from 'react-map-gl';
+
+import Map from './map';
+import Controls from './controls';
+
+function Root() {
+ return (
+
+
+
+
+ );
+}
+
+render(
, document.body.appendChild(document.createElement('div')));
diff --git a/examples/get-started/hook/controls.js b/examples/get-started/hook/controls.js
new file mode 100644
index 00000000..5d4c7151
--- /dev/null
+++ b/examples/get-started/hook/controls.js
@@ -0,0 +1,57 @@
+import * as React from 'react';
+
+import {useCallback, useState, useEffect} from 'react';
+import {useMap} from 'react-map-gl';
+
+export default function Controls() {
+ const {mymap} = useMap();
+ const [inputValue, setInputValue] = useState('');
+ const [hasError, setError] = useState(false);
+
+ useEffect(() => {
+ if (!mymap) {
+ return undefined;
+ }
+
+ const onMove = () => {
+ const {longitude, latitude} = mymap.getViewState();
+ setInputValue(`${longitude.toFixed(3)}, ${latitude.toFixed(3)}`);
+ setError(false);
+ };
+ mymap.on('move', onMove);
+ onMove();
+
+ return () => {
+ mymap.off('move', onMove);
+ };
+ }, [mymap]);
+
+ const onChange = useCallback(evt => {
+ setInputValue(evt.target.value);
+ }, []);
+
+ const onSubmit = useCallback(() => {
+ const [lng, lat] = inputValue.split(',').map(Number);
+ if (Math.abs(lng) <= 180 && Math.abs(lat) <= 85) {
+ mymap.easeTo({
+ center: [lng, lat],
+ duration: 1000
+ });
+ } else {
+ setError(true);
+ }
+ }, [mymap, inputValue]);
+
+ return (
+
+ MAP CENTER:
+
+
+
+ );
+}
diff --git a/examples/get-started/hook/map.js b/examples/get-started/hook/map.js
new file mode 100644
index 00000000..f7c0e222
--- /dev/null
+++ b/examples/get-started/hook/map.js
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import Map from 'react-map-gl';
+
+import 'mapbox-gl/dist/mapbox-gl.css';
+
+const MAPBOX_TOKEN = ''; // Set your mapbox token here
+
+export default function MapView() {
+ return (
+
+ );
+}
diff --git a/examples/get-started/hook/package.json b/examples/get-started/hook/package.json
new file mode 100644
index 00000000..c0dc4cc7
--- /dev/null
+++ b/examples/get-started/hook/package.json
@@ -0,0 +1,24 @@
+{
+ "scripts": {
+ "start": "webpack-dev-server --progress --hot --open",
+ "build": "webpack --mode=production -o ./dist"
+ },
+ "dependencies": {
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "react-map-gl": "^7.0.0",
+ "mapbox-gl": "^2.0.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.0.0",
+ "@babel/preset-env": "^7.0.0",
+ "@babel/preset-react": "^7.0.0",
+ "babel-loader": "^8.0.0",
+ "css-loader": "^6.5.0",
+ "html-webpack-plugin": "^5.5.0",
+ "style-loader": "^3.3.0",
+ "webpack": "^5.65.0",
+ "webpack-cli": "^4.9.0",
+ "webpack-dev-server": "^4.7.0"
+ }
+}
diff --git a/examples/get-started/hook/webpack.config.js b/examples/get-started/hook/webpack.config.js
new file mode 100644
index 00000000..f2fc1518
--- /dev/null
+++ b/examples/get-started/hook/webpack.config.js
@@ -0,0 +1,39 @@
+const webpack = require('webpack');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+module.exports = {
+ mode: 'development',
+
+ entry: {
+ app: './app.js'
+ },
+
+ devtool: 'source-map',
+
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ },
+ {
+ test: /\.js$/,
+ exclude: [/node_modules/],
+ use: [
+ {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/env', '@babel/react']
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ // Optional: Enables reading mapbox token from environment variable
+ plugins: [
+ new HtmlWebpackPlugin({title: 'react-map-gl Example'}),
+ new webpack.EnvironmentPlugin({MapboxAccessToken: ''})
+ ]
+};
diff --git a/examples/interaction/src/control-panel.tsx b/examples/interaction/src/control-panel.tsx
index 36edd7b7..08dd6f3b 100644
--- a/examples/interaction/src/control-panel.tsx
+++ b/examples/interaction/src/control-panel.tsx
@@ -28,7 +28,7 @@ function NumericInput({name, value, onChange}) {
}
function ControlPanel(props) {
- const {settings, interactionState, onChange} = props;
+ const {settings, onChange} = props;
const renderSetting = (name, value) => {
switch (typeof value) {
diff --git a/examples/viewport-animation/src/app.tsx b/examples/viewport-animation/src/app.tsx
index a05b53aa..3f4dbbed 100644
--- a/examples/viewport-animation/src/app.tsx
+++ b/examples/viewport-animation/src/app.tsx
@@ -19,10 +19,7 @@ export default function App() {
const mapRef = useRef
();
const onSelectCity = useCallback(({longitude, latitude}) => {
- if (mapRef.current) {
- const map = mapRef.current.getMap();
- map.flyTo({center: [longitude, latitude]});
- }
+ mapRef.current?.flyTo({center: [longitude, latitude], duration: 2000});
}, []);
return (
diff --git a/examples/zoom-to-bounds/src/app.tsx b/examples/zoom-to-bounds/src/app.tsx
index c9887389..ef8de0a8 100644
--- a/examples/zoom-to-bounds/src/app.tsx
+++ b/examples/zoom-to-bounds/src/app.tsx
@@ -17,12 +17,10 @@ export default function App() {
const onClick = (event: MapLayerMouseEvent) => {
const feature = event.features[0];
if (feature) {
- const map = mapRef.current.getMap();
-
// calculate the bounding box of the feature
const [minLng, minLat, maxLng, maxLat] = bbox(feature);
- map.fitBounds(
+ mapRef.current.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat]
diff --git a/src/components/attribution-control.ts b/src/components/attribution-control.ts
index 64421b2f..be095742 100644
--- a/src/components/attribution-control.ts
+++ b/src/components/attribution-control.ts
@@ -17,7 +17,7 @@ export type AttributionControlProps = {
};
function AttributionControl(props: AttributionControlProps): null {
- const ctrl = useControl(() => new mapboxgl.AttributionControl(props), props.position);
+ useControl(() => new mapboxgl.AttributionControl(props), {position: props.position});
return null;
}
diff --git a/src/components/fullscreen-control.ts b/src/components/fullscreen-control.ts
index 0ebc1e99..43bb521d 100644
--- a/src/components/fullscreen-control.ts
+++ b/src/components/fullscreen-control.ts
@@ -14,12 +14,12 @@ export type FullscreenControlProps = {
};
function FullscreenControl(props: FullscreenControlProps): null {
- const ctrl = useControl(
+ useControl(
() =>
new mapboxgl.FullscreenControl({
container: props.containerId && document.getElementById(props.containerId)
}),
- props.position
+ {position: props.position}
);
return null;
diff --git a/src/components/geolocate-control.ts b/src/components/geolocate-control.ts
index 069d01dd..fba37930 100644
--- a/src/components/geolocate-control.ts
+++ b/src/components/geolocate-control.ts
@@ -58,27 +58,30 @@ export type GeolocateControlProps = {
const GeolocateControl = forwardRef((props, ref) => {
const thisRef = useRef({props});
- const ctrl = useControl(() => {
- const gc = new mapboxgl.GeolocateControl(props);
+ const ctrl = useControl(
+ () => {
+ const gc = new mapboxgl.GeolocateControl(props);
- gc.on('geolocate', e => {
- thisRef.current.props.onGeolocate?.(e as GeolocateEvent);
- });
- gc.on('error', e => {
- thisRef.current.props.onError?.(e as GeolocateErrorEvent);
- });
- gc.on('outofmaxbounds', e => {
- thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateEvent);
- });
- gc.on('trackuserlocationstart', e => {
- thisRef.current.props.onTrackUserLocationStart?.(e as MapboxEvent);
- });
- gc.on('trackuserlocationend', e => {
- thisRef.current.props.onTrackUserLocationEnd?.(e as MapboxEvent);
- });
+ gc.on('geolocate', e => {
+ thisRef.current.props.onGeolocate?.(e as GeolocateEvent);
+ });
+ gc.on('error', e => {
+ thisRef.current.props.onError?.(e as GeolocateErrorEvent);
+ });
+ gc.on('outofmaxbounds', e => {
+ thisRef.current.props.onOutOfMaxBounds?.(e as GeolocateEvent);
+ });
+ gc.on('trackuserlocationstart', e => {
+ thisRef.current.props.onTrackUserLocationStart?.(e as MapboxEvent);
+ });
+ gc.on('trackuserlocationend', e => {
+ thisRef.current.props.onTrackUserLocationEnd?.(e as MapboxEvent);
+ });
- return gc;
- }, props.position) as mapboxgl.GeolocateControl;
+ return gc;
+ },
+ {position: props.position}
+ ) as mapboxgl.GeolocateControl;
thisRef.current.props = props;
diff --git a/src/components/layer.ts b/src/components/layer.ts
index f2c07623..80c498d9 100644
--- a/src/components/layer.ts
+++ b/src/components/layer.ts
@@ -1,5 +1,5 @@
import {useContext, useEffect, useMemo, useState, useRef} from 'react';
-import MapContext from './map-context';
+import {MapContext} from './map';
import assert from '../utils/assert';
import {deepEqual} from '../utils/deep-equal';
diff --git a/src/components/map-context.ts b/src/components/map-context.ts
deleted file mode 100644
index 1c8a44e9..00000000
--- a/src/components/map-context.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import * as React from 'react';
-import type {MapboxMap} from '../utils/types';
-
-export default React.createContext(null);
diff --git a/src/components/map.tsx b/src/components/map.tsx
index 99c9c638..30955c1f 100644
--- a/src/components/map.tsx
+++ b/src/components/map.tsx
@@ -1,17 +1,15 @@
import * as React from 'react';
-import {useState, useRef, useEffect, forwardRef, useImperativeHandle} from 'react';
+import {useState, useRef, useEffect, useContext, forwardRef, useImperativeHandle} from 'react';
-import Mapbox from '../mapbox/mapbox';
-import type {MapboxProps} from '../mapbox/mapbox';
-import MapContext from './map-context';
+import {MountedMapsContext} from './use-map';
+import Mapbox, {MapboxProps} from '../mapbox/mapbox';
+import createRef, {MapRef} from '../mapbox/create-ref';
import type {CSSProperties} from 'react';
import type {MapboxMap} from '../utils/types';
import useIsomorphicLayoutEffect from '../utils/use-isomorphic-layout-effect';
-export interface MapRef {
- getMap(): MapboxMap;
-}
+export const MapContext = React.createContext(null);
export type MapProps = MapboxProps & {
/** Map container id */
@@ -47,7 +45,8 @@ const defaultProps: MapProps = {
renderWorldCopies: true
};
-const Map = forwardRef((props: MapProps, ref) => {
+const Map = forwardRef((props, ref) => {
+ const mountedMapsContext = useContext(MountedMapsContext);
const [mapInstance, setMapInstance] = useState(null);
const containerRef = useRef();
@@ -55,7 +54,12 @@ const Map = forwardRef((props: MapProps, ref) => {
const map = new Mapbox(props);
map.initialize(containerRef.current);
setMapInstance(map);
- return () => map.destroy();
+ mountedMapsContext?.onMapMount(createRef(map), props.id);
+
+ return () => {
+ mountedMapsContext?.onMapUnmount(props.id);
+ map.destroy();
+ };
}, []);
useIsomorphicLayoutEffect(() => {
@@ -64,13 +68,7 @@ const Map = forwardRef((props: MapProps, ref) => {
}
});
- useImperativeHandle(
- ref,
- () => ({
- getMap: () => mapInstance.getMap()
- }),
- [mapInstance]
- );
+ useImperativeHandle(ref, () => createRef(mapInstance), [mapInstance]);
const style: CSSProperties = {
position: 'relative',
@@ -82,7 +80,7 @@ const Map = forwardRef((props: MapProps, ref) => {
return (
{mapInstance && (
- {props.children}
+ {props.children}
)}
);
diff --git a/src/components/marker.ts b/src/components/marker.ts
index c2764a7c..bdb8baf7 100644
--- a/src/components/marker.ts
+++ b/src/components/marker.ts
@@ -6,7 +6,7 @@ import {useEffect, useState, useRef, useContext} from 'react';
import mapboxgl from '../utils/mapboxgl';
import type {MarkerDragEvent, MarkerOptions, MapboxPopup} from '../utils/types';
-import MapContext from './map-context';
+import {MapContext} from './map';
import {arePointsEqual} from '../utils/deep-equal';
export type MarkerProps = Omit & {
diff --git a/src/components/navigation-control.ts b/src/components/navigation-control.ts
index 69b2639a..a5fd54e7 100644
--- a/src/components/navigation-control.ts
+++ b/src/components/navigation-control.ts
@@ -16,7 +16,7 @@ export type NavigationControlProps = {
};
function NavigationControl(props: NavigationControlProps): null {
- const ctrl = useControl(() => new mapboxgl.NavigationControl(props), props.position);
+ useControl(() => new mapboxgl.NavigationControl(props), {position: props.position});
return null;
}
diff --git a/src/components/popup.ts b/src/components/popup.ts
index 824a7709..cdf5ce3c 100644
--- a/src/components/popup.ts
+++ b/src/components/popup.ts
@@ -6,7 +6,7 @@ import {useEffect, useState, useRef, useContext} from 'react';
import mapboxgl from '../utils/mapboxgl';
import type {PopupOptions, MapboxEvent} from '../utils/types';
-import MapContext from './map-context';
+import {MapContext} from './map';
import {deepEqual} from '../utils/deep-equal';
export type PopupProps = PopupOptions & {
diff --git a/src/components/scale-control.ts b/src/components/scale-control.ts
index 3e8421e3..1b54d28a 100644
--- a/src/components/scale-control.ts
+++ b/src/components/scale-control.ts
@@ -19,10 +19,9 @@ const defaultProps: ScaleControlProps = {
};
function ScaleControl(props: ScaleControlProps): null {
- const ctrl = useControl(
- () => new mapboxgl.ScaleControl(props),
- props.position
- ) as mapboxgl.ScaleControl;
+ const ctrl = useControl(() => new mapboxgl.ScaleControl(props), {
+ position: props.position
+ }) as mapboxgl.ScaleControl;
// @ts-ignore
if (ctrl.options.unit !== props.unit || ctrl.options.maxWidth !== props.maxWidth) {
diff --git a/src/components/source.ts b/src/components/source.ts
index 22390470..81e9345e 100644
--- a/src/components/source.ts
+++ b/src/components/source.ts
@@ -1,7 +1,7 @@
import * as React from 'react';
import {useContext, useEffect, useMemo, useState, useRef} from 'react';
import {cloneElement} from 'react';
-import MapContext from './map-context';
+import {MapContext} from './map';
import assert from '../utils/assert';
import {deepEqual} from '../utils/deep-equal';
diff --git a/src/components/use-control.ts b/src/components/use-control.ts
index 62260043..90293e92 100644
--- a/src/components/use-control.ts
+++ b/src/components/use-control.ts
@@ -1,18 +1,30 @@
import {useContext, useState, useEffect} from 'react';
-import type {IControl, ControlPosition} from '../utils/types';
-import MapContext from './map-context';
+import type {IControl, ControlPosition, MapboxMap} from '../utils/types';
+import {MapContext} from './map';
-export default function useControl(onCreate: () => IControl, position?: ControlPosition) {
+export default function useControl(
+ onCreate: () => IControl,
+ opts?: {
+ position?: ControlPosition;
+ onAdd?: (map: MapboxMap) => void;
+ onRemove?: (map: MapboxMap) => void;
+ }
+) {
const map = useContext(MapContext);
- const [ctrl] = useState(onCreate);
+ const [ctrl] = useState(onCreate());
useEffect(() => {
- map.addControl(ctrl, position);
+ if (map) {
+ map.addControl(ctrl, opts?.position);
+ opts?.onAdd?.(map);
- return () => {
- map.removeControl(ctrl);
- };
- }, []);
+ return () => {
+ opts?.onRemove?.(map);
+ map.removeControl(ctrl);
+ };
+ }
+ return undefined;
+ }, [map]);
return ctrl;
}
diff --git a/src/components/use-map.tsx b/src/components/use-map.tsx
new file mode 100644
index 00000000..6ada4fb8
--- /dev/null
+++ b/src/components/use-map.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import {useState, useCallback, useContext} from 'react';
+
+import {MapRef} from '../mapbox/create-ref';
+
+type MountedMapsContextValue = {
+ maps: {[id: string]: MapRef};
+ onMapMount: (map: MapRef, id: string) => void;
+ onMapUnmount: (id: string) => void;
+};
+
+export const MountedMapsContext = React.createContext(null);
+
+export const MapProvider: React.FC<{}> = props => {
+ const [maps, setMaps] = useState<{[id: string]: MapRef}>({});
+
+ const onMapMount = useCallback(
+ (map: MapRef, id: string = 'default') => {
+ if (maps[id]) {
+ throw new Error(`Multiple maps with the same id: ${id}`);
+ }
+ setMaps({...maps, [id]: map});
+ },
+ [maps]
+ );
+
+ const onMapUnmount = useCallback(
+ (id: string = 'default') => {
+ const nextMaps = {...maps};
+ delete nextMaps[id];
+ setMaps(nextMaps);
+ },
+ [maps]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export function useMap() {
+ const {maps} = useContext(MountedMapsContext);
+ return maps;
+}
diff --git a/src/index.ts b/src/index.ts
index 8841bfaf..3cb8bd6b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,7 @@
export {default} from './components/map';
-export {default as Map, MapProps, MapRef} from './components/map';
+export {default as Map, MapProps} from './components/map';
+export {MapRef} from './mapbox/create-ref';
export {default as Marker, MarkerProps} from './components/marker';
export {default as Popup, PopupProps} from './components/popup';
@@ -22,5 +23,8 @@ export {default as ScaleControl, ScaleControlProps} from './components/scale-con
export {default as Source, SourceProps} from './components/source';
export {default as Layer, LayerProps} from './components/layer';
+export {default as useControl} from './components/use-control';
+export {MapProvider, useMap} from './components/use-map';
+
// Types
export * from './utils/types';
diff --git a/src/mapbox/create-ref.ts b/src/mapbox/create-ref.ts
new file mode 100644
index 00000000..ffd5d54e
--- /dev/null
+++ b/src/mapbox/create-ref.ts
@@ -0,0 +1,130 @@
+import type {MapboxMap, ViewState} from '../utils/types';
+import type Mapbox from './mapbox';
+
+/** mapboxgl.Map methods to forward to the ref object
+ Object.getOwnPropertyNames(Object.getPrototypeOf(map))
+ .filter(key => typeof temp1[key] === 'function' && key[0] != '_')
+ */
+const forwardMethods = [
+ // 'getCenter',
+ // 'setCenter',
+ 'panBy',
+ 'panTo',
+ // 'getZoom',
+ // 'setZoom',
+ 'zoomTo',
+ 'zoomIn',
+ 'zoomOut',
+ // 'getBearing',
+ // 'setBearing',
+ // 'getPadding',
+ // 'setPadding',
+ 'rotateTo',
+ 'resetNorth',
+ 'resetNorthPitch',
+ 'snapToNorth',
+ // 'getPitch',
+ // 'setPitch',
+ 'cameraForBounds',
+ 'fitBounds',
+ 'fitScreenCoordinates',
+ 'jumpTo',
+ 'getFreeCameraOptions',
+ 'setFreeCameraOptions',
+ 'easeTo',
+ 'flyTo',
+ 'isEasing',
+ 'stop',
+ // "addControl",
+ // "removeControl",
+ // "hasControl",
+ 'getContainer',
+ 'getCanvasContainer',
+ 'getCanvas',
+ 'resize',
+ 'getBounds',
+ // "getMaxBounds",
+ // "setMaxBounds",
+ // "setMinZoom",
+ // "getMinZoom",
+ // "setMaxZoom",
+ // "getMaxZoom",
+ // "setMinPitch",
+ // "getMinPitch",
+ // "setMaxPitch",
+ // "getMaxPitch",
+ // "getRenderWorldCopies",
+ // "setRenderWorldCopies",
+ // "getProjection",
+ // "setProjection",
+ 'project',
+ 'unproject',
+ 'isMoving',
+ 'isZooming',
+ 'isRotating',
+ 'on',
+ 'once',
+ 'off',
+ 'queryRenderedFeatures',
+ 'querySourceFeatures',
+ 'queryTerrainElevation',
+ // "setStyle",
+ // "getStyle",
+ 'isStyleLoaded',
+ // "addSource",
+ 'isSourceLoaded',
+ 'areTilesLoaded',
+ // "addSourceType",
+ // "removeSource",
+ 'getSource',
+ 'addImage',
+ 'updateImage',
+ 'hasImage',
+ 'removeImage',
+ 'loadImage',
+ 'listImages',
+ // "addLayer",
+ 'moveLayer',
+ // "removeLayer",
+ 'getLayer',
+ // "setLayerZoomRange",
+ // "setFilter",
+ // "getFilter",
+ // "setPaintProperty",
+ // "getPaintProperty",
+ // "setLayoutProperty",
+ // "getLayoutProperty",
+ 'setLight',
+ 'getLight',
+ 'setTerrain',
+ 'getTerrain',
+ 'setFog',
+ 'getFog',
+ 'setFeatureState',
+ 'removeFeatureState',
+ 'getFeatureState',
+ 'loaded'
+ // "remove",
+ // "triggerRepaint"
+] as const;
+
+export type MapRef = {
+ getMap(): MapboxMap;
+ getViewState(): ViewState;
+} & Pick;
+
+export default function createRef(mapInstance: Mapbox): MapRef {
+ if (!mapInstance) {
+ return null;
+ }
+
+ const result: any = {
+ getMap: () => mapInstance.map,
+ getViewState: () => mapInstance.viewState
+ };
+ for (const key of forwardMethods) {
+ result[key] = mapInstance.map[key].bind(mapInstance.map);
+ }
+
+ return result;
+}
diff --git a/src/mapbox/mapbox.ts b/src/mapbox/mapbox.ts
index dc3166d7..8fc1426b 100644
--- a/src/mapbox/mapbox.ts
+++ b/src/mapbox/mapbox.ts
@@ -19,7 +19,8 @@ import type {
MapDataEvent,
MapboxEvent,
ErrorEvent,
- MapboxGeoJSONFeature
+ MapboxGeoJSONFeature,
+ MapboxMap
} from '../utils/types';
export type MapboxProps = Omit<
@@ -198,8 +199,12 @@ export default class Mapbox {
this.props = props;
}
- getMap() {
- return this._map;
+ get map(): MapboxMap {
+ return this._map as MapboxMap;
+ }
+
+ get viewState(): ViewState {
+ return transformToViewState(this._renderTransform);
}
setProps(props: MapboxProps) {