Интеграция картографического движка MapLibre GL JS в приложения на React требует строгого разделения ответственности между жизненным циклом карты и жизненным циклом компонентов интерфейса. Основная сложность заключается в том, что карта представляет собой императивный объект, тогда как React оперирует декларативной моделью обновлений.
Правильная организация hooks позволяет:
Фундаментальный подход заключается в создании useRef для
хранения экземпляра карты и useEffect для
инициализации.
import { useEffect, useRef, useState } from "react";
import maplibregl from "maplibre-gl";
export function useMapLibre(containerRef, options) {
const mapRef = useRef(null);
const [map, setMap] = useState(null);
useEffect(() => {
if (!containerRef.current) return;
if (mapRef.current) return;
mapRef.current = new maplibregl.Map({
container: containerRef.current,
style: options.style,
center: options.center,
zoom: options.zoom,
});
setMap(mapRef.current);
return () => {
mapRef.current.remove();
mapRef.current = null;
setMap(null);
};
}, [containerRef]);
return map;
}
Ключевая идея заключается в том, что mapRef хранит
императивный экземпляр, а React-состояние используется только как сигнал
готовности карты для других хуков.
Частая ошибка — попытка передавать параметры карты напрямую через
props после её создания. MapLibre не является реактивным API, поэтому
изменения должны применяться через методы setStyle,
setCenter, setZoom.
Для этого вводится слой синхронизации:
useEffect(() => {
if (!map) return;
map.setCenter(options.center);
}, [map, options.center]);
useEffect(() => {
if (!map) return;
map.setZoom(options.zoom);
}, [map, options.zoom]);
Такой подход предотвращает пересоздание карты и сохраняет стабильность WebGL-контекста.
Источники (sources) в MapLibre являются ключевым
элементом архитектуры карты. Их можно обернуть в отдельный hook.
export function useGeoSource(map, id, data) {
useEffect(() => {
if (!map || !data) return;
if (map.getSource(id)) {
map.getSource(id).setData(data);
} else {
map.addSource(id, {
type: "geojson",
data,
});
}
}, [map, id, data]);
}
Данный паттерн позволяет безопасно обновлять данные без пересоздания слоя и без нарушения связей между слоями и источниками.
Слои требуют строгого контроля порядка добавления и удаления. Использование hook-обёртки упрощает управление:
export function useMapLayer(map, layer) {
useEffect(() => {
if (!map || !layer) return;
if (!map.getLayer(layer.id)) {
map.addLayer(layer);
}
return () => {
if (map.getLayer(layer.id)) {
map.removeLayer(layer.id);
}
};
}, [map, layer]);
}
Важно учитывать, что слой всегда зависит от существующего источника, поэтому порядок инициализации критичен.
События MapLibre можно абстрагировать в реактивную модель:
export function useMapEvent(map, event, handler) {
useEffect(() => {
if (!map || !handler) return;
map.on(event, handler);
return () => {
map.off(event, handler);
};
}, [map, event, handler]);
}
Такой подход позволяет интегрировать события карты в систему React без ручного управления подписками.
При сложных интерфейсах возникает необходимость синхронизации состояния карты с глобальным состоянием приложения (например, Redux или Zustand). Основная проблема — предотвращение циклических обновлений.
Решение заключается в разделении источника истины:
useEffect(() => {
if (!map) return;
const onMove End = () => {
const center = map.getCenter();
const zoom = map.getZoom();
// обновление внешнего состояния
setViewState({ center, zoom });
};
map.on("moveend", onMoveEnd);
return () => map.off("moveend", onMoveEnd);
}, [map]);
При работе с MapLibre критически важно избегать пересоздания объектов:
useMemouseCallbackconst layer = useMemo(() => ({
id: "points",
type: "circle",
source: "points",
paint: {
"circle-radius": 6,
"circle-color": "#1978c8",
},
}), []);
Это предотвращает лишние эффекты и повторное применение конфигураций.
В сложных SPA-сценариях карта может создаваться только при наличии видимого контейнера. Это решается через условный mount:
const [ready, setReady] = useState(false);
useEffect(() => {
if (!containerRef.current) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setReady(true);
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
После активации ready инициируется создание карты через
основной hook.
Полноценная архитектура строится через композицию специализированных hooks:
useMapLibre — создание картыuseMapEvent — событияuseGeoSource — данныеuseMapLayer — визуализацияuseViewSync — синхронизация состоянияПример связки:
const map = useMapLibre(containerRef, options);
useGeoSource(map, "cities", citiesData);
useMapLayer(map, citiesLayer);
useMapEvent(map, "click", onMapClick);
Такая структура обеспечивает модульность и предсказуемость поведения.
При работе с несколькими экземплярами карт требуется изоляция состояния. Использование контекста React позволяет централизовать доступ:
const MapContext = createContext(null);
export function MapProvider({ children }) {
const containerRef = useRef(null);
const map = useMapLibre(containerRef, defaultOptions);
return (
<MapContext.Provider value={map}>
<div ref={containerRef} />
{children}
</MapContext.Provider>
);
}
Это позволяет дочерним компонентам использовать карту без передачи props через дерево.
Особое внимание требуется очистке ресурсов:
map.remove()Нарушение этих правил приводит к накоплению WebGL-контекстов и деградации производительности.
MapLibre поддерживает динамическую загрузку стилей, что требует отдельного контроля:
useEffect(() => {
if (!map) return;
map.setStyle(styleUrl);
}, [map, styleUrl]);
При смене стиля необходимо учитывать, что слои и источники удаляются,
поэтому их восстановление должно быть реактивным после события
styledata.
Кастомные UI-элементы карты удобно инкапсулировать:
export function useMapControl(map, control) {
useEffect(() => {
if (!map || !control) return;
map.addControl(control);
return () => {
map.removeControl(control);
};
}, [map, control]);
}
Это обеспечивает единый жизненный цикл между React-компонентами и контролами MapLibre.
В результате применения hooks карта перестаёт быть «внешним объектом» и становится управляемой частью состояния приложения. Однако её поведение остаётся императивным внутри, что требует строгой дисциплины:
Такой подход позволяет масштабировать картографические интерфейсы от простых визуализаций до сложных геоинформационных систем без потери управляемости.