Tips and Tricks
Securing Mapbox Token
Because Mapbox tokens are required for the client application to make requests to Mapbox servers, you have to distribute it with your app. It is not possible to stop a visitor to your site from scraping the token. The practice outlined below can help you protect your token from being abused.
- Never commit your token in clear text into GitHub or other source control.
- In your local dev environment, define the token in an environment variable e.g.
MapboxAccessTokenDev=...
in the command line, or use something like dotenv and putMapboxAccessTokenDev=...
in a.env
file. Add.env
to.gitignore
so it's never tracked. If your app is deployed by a continuous integration pipeline, follow its documentation and set a secret environment variable. - Create separate tokens for development (often times on
http://localhost
), public code snippet (Gist, Codepen etc.) and production (deployed tohttps://mycompany.com
). The public token should be rotated regularly. The production token should have strict scope and URL restrictions that only allows it to be used on a domain that you own. - Add the following to your bundler config:
- Webpack
- Rollup
/// webpack.config.js
const {DefinePlugin} = require('webpack');
module.exports = {
...
plugins: [
new DefinePlugin({
'process.env.MapboxAccessToken': JSON.stringify(process.env.NODE_ENV == 'production' ? process.env.MapboxAccessTokenProd : process.env.MapboxAccessTokenDev)
})
]
};
/// rollup.config.js
const replace = require('@rollup/plugin-replace').default;
module.exports = {
...
plugins: [
replace({
'process.env.MapboxAccessToken': JSON.stringify(process.env.NODE_ENV == 'production' ? process.env.MapboxAccessTokenProd : process.env.MapboxAccessTokenDev)
})
]
};
react-map-gl automatically picks up process.env.MapboxAccessToken
or process.env.REACT_APP_MAPBOX_ACCESS_TOKEN
if they are defined. Alternatively, you can use your own variable name (e.g. __SUPER_SECRET_TOKEN__
) and pass it in manually with mapboxAccessToken={__SUPER_SECRET_TOKEN__}
.
Minimize Cost from Frequent Re-mounting
In a moderately complex single-page app, as the user navigates through the UI, a map component may unmount and mount again many times during a session. Consider the following layout:
/// Example using Tabs from Material UI
<TabContext value={selectedTab}>
<TabList onChange={handleTabChange}>
<Tab label="Map" value="map" />
<Tab label="List" value="table" />
</TabList>
<TabPanel value="map">
<Map mapStyle="mapbox://styles/mapbox/streets-v9" >
{items.map(renderMarker)}
</Map>
</TabPanel>
<TabPanel value="table">
<Table>
{items.map(renderRow)}
</Table>
</TabPanel>
</TabContext>
Every time the user clicks the "table" tab, the map is unmounted. When they click the "map" tab, the map is mounted again. As of v2.0, mapbox-gl generates a billable event every time a Map object is initialized. It is obviously not ideal to get billed for just collapsing and expanding part of the UI.
In this case, it is recommended that you set the reuseMaps prop to true
:
<TabPanel value="map">
<Map reuseMaps mapStyle="mapbox://styles/mapbox/streets-v9" >
{items.map(renderMarker)}
</Map>
</TabPanel>
This bypasses the initialization when a map is removed then added back.
Performance with Many Markers
If your application uses externally managed camera state, like with Redux, the number of React rerenders may be very high when the user is interacting with the map. Consider the following setup:
import {useSelector, useDispatch} from 'react-redux';
import Map, {Marker} from 'react-map-gl';
function MapView() {
const viewState = useSelector((s: RootState) => s.viewState);
const vehicles = useSelector((s: RootState) => s.vehicles);
const dispatch = useDispatch();
const onMove = useCallback(evt => {
dispatch({type: 'setViewState', payload: evt.viewState});
}, []);
return (
<Map
{...viewState}
onMove={onMove}
mapStyle="mapbox://styles/mapbox/streets-v9" >
>
{vehicles.map(vehicle => (
<Marker key={vehicle.id}
longitude={vehicle.coordinates[0]}
latitude={vehicle.coordinates[1]}>
<svg>
// vehicle icon
</svg>
</Marker>)
)}
</Map>
);
}
This component is rerendered on every animation frame when the user is dragging the map. If it's trying to render hundreds of markers, the performance lag will become quite visible.
One way to improve the performance is useMemo
:
const markers = useMemo(() => vehicles.map(vehicle => (
<Marker key={vehicle.id}
longitude={vehicle.coordinates[0]}
latitude={vehicle.coordinates[1]}>
<svg>
// vehicle icon
</svg>
</Marker>)
), [vehicles]);
return (
<Map
{...viewState}
onMove={onMove}
mapStyle="mapbox://styles/mapbox/streets-v9" >
>
{markers}
</Map>
);
}
This prevents React from rerendering the markers unless they have changed.
If your application can do without complicated DOM objects and CSS styling, consider switching to a symbol layer. Layers are rendered in WebGL and are much more performant than markers:
const vehiclesGeoJSON = useMemo(() => {
return {
type: 'FeatureCollection',
features: vehicles.map(vehicle => turf.point(vehicle.coordinates, vehicle))
};
}, [vehicles]);
return (
<Map
{...viewState}
onMove={onMove}
mapStyle="mapbox://styles/mapbox/streets-v9" >
>
<Source id="vehicles" type="geojson" data={vehiclesGeoJSON}>
<Layer type="symbol"
layout={{
'icon-image': 'vehicle-icon',
'icon-size': 1,
'text-field': ['get', 'id']
}}
/>
</Source>
</Map>
);
Finding out if a point is within the current viewport
There are some situations where you want to know if a point is currently visible on the map.
Checking this is simple and can be done like so:
const mapRef = useRef<MapRef>();
const checkIfPositionInViewport = (lat, lng) => {
const bounds = mapRef.current.getBounds();
return bounds.contains([lng, lat]);
}
return <Map ref={mapRef} ... />