Mapbox GL JS представляет собой императивную библиотеку визуализации карт, тогда как React построен вокруг декларативной модели интерфейса. Основная сложность интеграции заключается в правильном управлении жизненным циклом объекта карты.
В React компоненты могут многократно перерисовываться, однако
экземпляр карты должен создаваться только один раз. По этой причине для
хранения карты обычно используются ссылки (useRef), а
инициализация выполняется внутри эффекта (useEffect).
Типичная схема взаимодействия выглядит следующим образом:
Mapbox GL JS.ref.Установка библиотеки выполняется через npm:
npm install mapbox-gl
или через Yarn:
yarn add mapbox-gl
Дополнительно необходимо подключить стили:
import 'mapbox-gl/dist/mapbox-gl.css';
Наиболее распространённый вариант интеграции основан на функциональных компонентах и хуках.
import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
mapboxgl.accessToken = 'YOUR_TOKEN';
function Map() {
const mapContainer = useRef(null);
const map = useRef(null);
useEffect(() => {
if (map.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: [37.6176, 55.7558],
zoom: 10
});
return () => {
map.current?.remove();
};
}, []);
return (
<div
ref={mapContainer}
style={{
width: '100%',
height: '500px'
}}
/>
);
}
export default Map;
В примере используются две ссылки:
const mapContainer = useRef(null);
const map = useRef(null);
Первая хранит DOM-элемент контейнера.
Вторая хранит экземпляр объекта Map, созданного
библиотекой.
Без использования useRef карта пересоздавалась бы при
каждом рендере компонента.
Создание выполняется внутри эффекта:
useEffect(() => {
map.current = new mapboxgl.Map({...});
}, []);
Пустой массив зависимостей гарантирует однократное выполнение.
Mapbox создаёт большое количество внутренних ресурсов:
При удалении компонента необходимо вызвать:
map.current.remove();
Это предотвращает утечки памяти.
React может управлять параметрами карты через состояние.
Пример хранения координат:
const [center, setCenter] = useState([37.6176, 55.7558]);
Обновление карты при изменении состояния:
useEffect(() => {
if (!map.current) return;
map.current.setCenter(center);
}, [center]);
Изменение состояния автоматически приводит к обновлению положения карты.
Нередко требуется обратная связь — карта изменяет состояние React.
Например, отслеживание текущего центра:
map.current.on('move', () => {
const center = map.current.getCenter();
setLng(center.lng.toFixed(4));
setLat(center.lat.toFixed(4));
});
Полный пример:
const [lng, setLng] = useState(0);
const [lat, setLat] = useState(0);
const [zoom, setZoom] = useState(0);
useEffect(() => {
map.current.on('move', () => {
const center = map.current.getCenter();
setLng(center.lng.toFixed(4));
setLat(center.lat.toFixed(4));
setZoom(map.current.getZoom().toFixed(2));
});
}, []);
Отображение информации:
<div>
Longitude: {lng}
Latitude: {lat}
Zoom: {zoom}
</div>
При сложной конфигурации карты полезно мемоизировать параметры.
const mapOptions = useMemo(() => ({
style: 'mapbox://styles/mapbox/light-v11',
center: [30.3158, 59.9398],
zoom: 11
}), []);
Создание карты:
map.current = new mapboxgl.Map({
container: mapContainer.current,
...mapOptions
});
Это предотвращает создание новых объектов конфигурации при каждом рендере.
Повторяющаяся логика может быть вынесена в собственный хук.
import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
export function useMapbox(options) {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current) return;
mapRef.current = new mapboxgl.Map({
container: containerRef.current,
...options
});
return () => mapRef.current.remove();
}, []);
return {
containerRef,
mapRef
};
}
const { containerRef, mapRef } = useMapbox({
style: 'mapbox://styles/mapbox/dark-v11',
center: [0, 0],
zoom: 2
});
return <div ref={containerRef} />;
Такой подход повышает переиспользуемость кода.
map.current.on('load', () => {
new mapboxgl.Marker()
.setLngLat([37.6176, 55.7558])
.addTo(map.current);
});
Массив объектов:
const markers = [
{
id: 1,
lng: 37.6176,
lat: 55.7558
},
{
id: 2,
lng: 30.3158,
lat: 59.9398
}
];
Создание:
markers.forEach(marker => {
new mapboxgl.Marker()
.setLngLat([marker.lng, marker.lat])
.addTo(map.current);
});
const [locations, setLocations] = useState([]);
После получения данных:
useEffect(() => {
if (!map.current) return;
locations.forEach(location => {
new mapboxgl.Marker()
.setLngLat([
location.lng,
location.lat
])
.addTo(map.current);
});
}, [locations]);
На практике необходимо также реализовывать удаление старых маркеров во избежание дублирования.
React часто получает данные через API и передаёт их карте.
const geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
37.6176,
55.7558
]
}
}
]
};
map.current.addSource('points', {
type: 'geojson',
data: geojson
});
map.current.addLayer({
id: 'points-layer',
type: 'circle',
source: 'points',
paint: {
'circle-radius': 8,
'circle-color': '#ff0000'
}
});
const [data, setData] = useState(initialGeoJson);
Обновление источника:
useEffect(() => {
if (!map.current) return;
const source = map.current.getSource('points');
if (source) {
source.setData(data);
}
}, [data]);
Преимущество такого подхода заключается в отсутствии необходимости пересоздавать слой.
map.current.on('click', event => {
console.log(event.lngLat);
});
map.current.on('load', () => {
console.log('Map loaded');
});
map.current.on('zoom', () => {
console.log(map.current.getZoom());
});
Для корректной работы React желательно сохранять обработчики.
useEffect(() => {
const handleMove = () => {
console.log('moving');
};
map.current.on('move', handleMove);
return () => {
map.current.off('move', handleMove);
};
}, []);
Такой подход предотвращает накопление лишних обработчиков.
Получение объектов с сервера:
useEffect(() => {
async function loadData() {
const response = await fetch('/api/points');
const data = await response.json();
setGeoJson(data);
}
loadData();
}, []);
После обновления состояния источник карты автоматически получает
новые данные через setData().
В больших приложениях карта часто используется несколькими компонентами одновременно.
Создание контекста:
import { createContext } from 'react';
export const MapContext = createContext(null);
Провайдер:
<MapContext.Provider value={mapRef}>
{children}
</MapContext.Provider>
Получение карты:
const mapRef = useContext(MapContext);
Это позволяет избежать передачи экземпляра карты через множество уровней компонентов.
При использовании маршрутизации карта может:
Пример ленивой загрузки:
const MapPage = lazy(() => import('./MapPage'));
Это уменьшает размер первоначального JavaScript-бандла.
Mapbox GL JS зависит от браузерных API:
Поэтому при использовании SSR требуется создавать карту только на клиенте.
Проверка:
if (typeof window !== 'undefined') {
// создание карты
}
Для Next.js часто используется динамический импорт:
import dynamic from 'next/dynamic';
const Map = dynamic(
() => import('./Map'),
{
ssr: false
}
);
Неправильно:
const [map, setMap] = useState(null);
Правильно:
const mapRef = useRef(null);
Объект карты не участвует в процессе рендеринга интерфейса.
Избегать обновления состояния при каждом пикселе перемещения карты.
Плохо:
map.on('move', () => {
setCenter(map.getCenter());
});
Лучше:
map.on('moveend', () => {
setCenter(map.getCenter());
});
Для тяжёлых дочерних компонентов:
export default React.memo(ControlPanel);
Это уменьшает количество лишних обновлений интерфейса.
Пример структуры проекта:
src/
├── components/
│ ├── Map/
│ │ ├── Map.jsx
│ │ ├── MarkerLayer.jsx
│ │ ├── RouteLayer.jsx
│ │ └── Controls.jsx
│
├── hooks/
│ ├── useMapbox.js
│ └── useGeoJson.js
│
├── context/
│ └── MapContext.js
│
├── pages/
│ └── Dashboard.jsx
│
└── services/
└── api.js
Подобная организация позволяет разделять ответственность между компонентами, слоями данных и логикой взаимодействия с картой, сохраняя код масштабируемым даже при разработке крупных геоинформационных приложений на основе React и Mapbox GL JS.