Use Mapbox GL JS in a React app
Mapbox provides an excellent tutorial for implementing mapbox-gl with React. We would like to build on this by creating a reusable hook component. Our maps will not have a consistent appearance — there will be variations in container sizes, controls, and data displayed.
Requirements:
- A centralized location for instantiating the map. We want to avoid duplicating the map setup and connection between React and mapbox-gl.
- Flexibility in map configurations.
- Access to the map instance so we can add data and event handlers.
- Ability to control the dimensions of the map container.
import React, { useRef, useState, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
// Be sure to replace this with your own token
mapboxgl.accessToken = 'MY_ACCESS_TOKEN';
export default function Map({
center,
zoom = 17,
width = '100%',
height = '300px',
onInit,
}) {
const ref = useRef(null);
const [map, setMap] = useState(null);
useEffect(() => {
// Don't create the map until the ref is connected to the container div.
// Also don't create the map if it's already been created.
if (ref.current && !map) {
const map = new mapboxgl.Map({
container: ref.current,
style: 'mapbox://styles/mapbox/streets-v11',
center,
zoom,
});
setMap(map);
onInit(map);
}
}, [ref, center, zoom, map, onInit]);
return <div ref={ref} style={{ width, height }} />;
}
That’s a great start, but it still only lets us modify the width and height of the map container. For most cases that should be sufficient, but if you require more customization there are two options:
Option 1: Add a className Prop to the Component
Including a className
prop enables specifying a custom style for the map container. You could also opt for accepting a styles
prop if you prefer inline styling, but I tend to favor using className
.
import React, { useRef, useState, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
// Be sure to replace this with your own token
mapboxgl.accessToken = 'MY_ACCESS_TOKEN';
export default function Map({
center,
zoom = 17,
className,
onInit,
}) {
const ref = useRef(null);
const [map, setMap] = useState(null);
useEffect(() => {
// Don't create the map until the ref is connected to the container div.
// Also don't create the map if it's already been created.
if (ref.current && !map) {
const map = new mapboxgl.Map({
container: ref.current,
style: 'mapbox://styles/mapbox/streets-v11',
center,
zoom,
});
setMap(map);
onInit(map);
}
}, [ref, center, zoom, map, onInit]);
return <div ref={ref} classname={className} />;
}
Option 2: Convert This to a Custom Hook
Converting this to a custom hook would allow centralizing the map instantiation while still providing full control over the container the map is attached to.
/**
* useMap.js
*/
import mapboxgl from 'mapbox-gl';
import React, { useRef, useEffect, useState } from 'react';
// Be sure to replace this with your own token
mapboxgl.accessToken = 'MY_ACCESS_TOKEN';
export default function useMapbox({
center,
zoom = 17,
onInit
}) {
const ref = useRef(null);
const [map, setMap] = useState(null);
useEffect(() => {
if (ref.current && !map) {
const map = new mapboxgl.Map({
container: ref.current,
style: 'mapbox://styles/mapbox/streets-v11',
center,
zoom,
});
setMap(map);
onInit(map);
}
}, [ref, center, zoom, map]);
return { ref };
}
Then we use the hook in a component:
import useMap from './useMap';
function FancyMap() {
const onInitHandler = map => {
// Add data and events here
}
const { ref } = useMap({ center, zoom, onInit: onInitHandler });
return <div ref={ref} style={{ width: '100%', height: '300px' }} />;
}