Mapbox GL JS управляет картой через собственный жизненный цикл и напрямую взаимодействует с DOM-элементом контейнера. React, напротив, использует декларативную модель рендеринга и виртуальный DOM. По этой причине интеграция карты в React почти всегда строится вокруг хуков, которые позволяют контролировать создание, обновление и уничтожение экземпляра карты.
Основная задача React hooks при работе с Mapbox GL JS — корректно синхронизировать состояние приложения и состояние карты без лишних перерисовок и утечек памяти.
Наиболее часто используются следующие хуки:
useRefuseEffectuseStateuseMemouseCallbackЭкземпляр карты не должен пересоздаваться при каждом рендере
компонента. Для хранения ссылки на карту применяется
useRef.
import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
function Map() {
const mapContainer = useRef(null);
const map = useRef(null);
useEffect(() => {
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={{ height: '500px' }} />;
}
В данном примере:
mapContainer хранит DOM-узел контейнера.map хранит экземпляр карты.Хук useEffect является основным инструментом управления
жизненным циклом Mapbox GL JS.
Инициализация карты выполняется после появления контейнера в DOM:
useEffect(() => {
const mapInstance = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/light-v11'
});
return () => {
mapInstance.remove();
};
}, []);
Пустой массив зависимостей гарантирует однократное выполнение эффекта.
Если не удалить карту через remove(), останутся:
Это может привести к утечкам памяти.
Некоторые параметры карты удобно сохранять в состоянии React.
Пример хранения координат центра:
const [center, setCenter] = useState({
lng: 37.6176,
lat: 55.7558
});
После перемещения карты состояние обновляется:
useEffect(() => {
map.current.on('move', () => {
const center = map.current.getCenter();
setCenter({
lng: center.lng,
lat: center.lat
});
});
}, []);
Отображение координат:
<div>
{center.lng.toFixed(4)}, {center.lat.toFixed(4)}
</div>
Таким образом React-компоненты получают актуальные данные о положении карты.
Масштаб часто используется в интерфейсе для отображения текущего уровня приближения.
const [zoom, setZoom] = useState(10);
Обновление значения:
useEffect(() => {
map.current.on('zoom', () => {
setZoom(map.current.getZoom());
});
}, []);
Использование:
<p>Zoom: {zoom.toFixed(2)}</p>
Состояние React автоматически отражает изменения карты.
Для сложных интерфейсов часто требуется получать актуальные границы карты.
const [bounds, setBounds] = useState(null);
useEffect(() => {
map.current.on('moveend', () => {
const currentBounds = map.current.getBounds();
setBounds({
north: currentBounds.getNorth(),
south: currentBounds.getSouth(),
east: currentBounds.getEast(),
west: currentBounds.getWest()
});
});
}, []);
Полученные данные используются для:
При работе с большим количеством событий желательно предотвращать создание новых функций при каждом рендере.
Без оптимизации:
const handleMove = () => {
console.log('move');
};
С оптимизацией:
const handleMove = useCallback(() => {
console.log('move');
}, []);
Подключение:
useEffect(() => {
map.current.on('move', handleMove);
return () => {
map.current.off('move', handleMove);
};
}, [handleMove]);
Преимущества:
При большом количестве геоданных могут выполняться дорогостоящие вычисления.
Например, формирование GeoJSON:
const geojson = useMemo(() => {
return {
type: 'FeatureCollection',
features: points.map(point => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [point.lng, point.lat]
}
}))
};
}, [points]);
GeoJSON будет пересчитываться только при изменении массива
points.
Добавление источника выполняется после загрузки стиля.
useEffect(() => {
map.current.on('load', () => {
map.current.addSource('cities', {
type: 'geojson',
data: geojson
});
});
}, [geojson]);
Обновление данных:
useEffect(() => {
const source = map.current.getSource('cities');
if (source) {
source.setData(geojson);
}
}, [geojson]);
Такой подход позволяет использовать React как источник данных для карты.
Добавление слоя:
useEffect(() => {
if (!map.current) return;
const addLayer = () => {
map.current.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': 6,
'circle-color': '#ff0000'
}
});
};
map.current.on('load', addLayer);
return () => {
if (map.current.getLayer('cities-layer')) {
map.current.removeLayer('cities-layer');
}
};
}, []);
React контролирует жизненный цикл слоя аналогично жизненному циклу компонентов.
При большом количестве карт код удобно выносить в пользовательские хуки.
Пример:
import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
export function useMap(options) {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
mapRef.current = new mapboxgl.Map({
container: containerRef.current,
...options
});
return () => {
mapRef.current.remove();
};
}, []);
return {
containerRef,
mapRef
};
}
Использование:
function MapComponent() {
const { containerRef } = useMap({
style: 'mapbox://styles/mapbox/streets-v12',
center: [37.6176, 55.7558],
zoom: 10
});
return (
<div
ref={containerRef}
style={{ height: '500px' }}
/>
);
}
Преимущества:
Специализированный хук может отслеживать только центр карты.
function useMapCenter(mapRef) {
const [center, setCenter] = useState(null);
useEffect(() => {
if (!mapRef.current) return;
const updateCenter = () => {
const center = mapRef.current.getCenter();
setCenter({
lng: center.lng,
lat: center.lat
});
};
mapRef.current.on('move', updateCenter);
return () => {
mapRef.current.off('move', updateCenter);
};
}, [mapRef]);
return center;
}
Подобная декомпозиция делает код значительно более читаемым.
Создание маркера:
const markerRef = useRef(null);
useEffect(() => {
markerRef.current = new mapboxgl.Marker()
.setLngLat([37.6176, 55.7558])
.addTo(map.current);
return () => {
markerRef.current.remove();
};
}, []);
Обновление координат:
useEffect(() => {
if (!markerRef.current) return;
markerRef.current.setLngLat([
longitude,
latitude
]);
}, [longitude, latitude]);
React управляет состоянием маркера через свойства компонента.
Часто координаты приходят извне.
function Map({ center }) {
// ...
}
Синхронизация:
useEffect(() => {
if (!map.current) return;
map.current.flyTo({
center
});
}, [center]);
Каждое изменение свойства вызывает плавное перемещение карты.
Переход к новой точке:
useEffect(() => {
map.current.flyTo({
center: destination,
zoom: 14,
duration: 2000
});
}, [destination]);
Это распространённый паттерн для:
Распространённая ошибка — хранить экземпляр карты в состоянии.
Неправильно:
const [map, setMap] = useState(null);
Правильно:
const mapRef = useRef(null);
Причина проста: изменение состояния вызывает рендеринг компонента,
тогда как изменение ref происходит без повторного
рендера.
Для объектов Mapbox GL JS рекомендуется использовать именно
useRef.
Каждый обработчик должен удаляться при размонтировании компонента.
useEffect(() => {
const handleClick = event => {
console.log(event.lngLat);
};
map.current.on('click', handleClick);
return () => {
map.current.off('click', handleClick);
};
}, []);
Такой подход предотвращает:
При наличии нескольких компонентов может потребоваться единый доступ к карте.
Создание контекста:
import { createContext } from 'react';
export const MapContext = createContext(null);
Провайдер:
<MapContext.Provider value={mapRef}>
{children}
</MapContext.Provider>
Получение карты:
const mapRef = useContext(MapContext);
Это позволяет получать доступ к карте из любого дочернего компонента без передачи пропсов через множество уровней вложенности.
В крупных проектах обычно формируется набор специализированных хуков:
useMap()
useMapCenter()
useMapBounds()
useMapZoom()
useMapMarkers()
useMapLayers()
useMapSources()
useMapEvents()
useMapControls()
Каждый хук отвечает за отдельную часть функциональности:
Подобная архитектура обеспечивает высокую масштабируемость приложения, упрощает тестирование и делает интеграцию Mapbox GL JS с React предсказуемой и удобной для сопровождения даже в проектах с большим количеством картографических компонентов и сложной бизнес-логикой.