Интеграция MapLibre GL JS в React строится вокруг императивного API карты внутри декларативной модели UI. Карта остаётся внешним объектом, управляемым через жизненный цикл компонентов, refs и эффекты, тогда как React отвечает за конфигурацию, состояние и синхронизацию событий.
Ключевая особенность подхода — разделение ответственности:
useEffect) и ссылки
(useRef)Основной паттерн — создание DOM-узла и привязка к нему экземпляра карты.
import { useEffect, useRef } from "react";
import maplibregl from "maplibre-gl";
export function MapView() {
const mapContainer = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current) return;
mapRef.current = new maplibregl.Map({
container: mapContainer.current,
style: "https://demotiles.maplibre.org/style.json",
center: [30.3158, 59.9398],
zoom: 10
});
return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, []);
return <div ref={mapContainer} style={{ width: "100%", height: "100vh" }} />;
}
useRef хранит DOM-элемент контейнера картыuseRef хранит экземпляр карты, избегая повторной
инициализацииuseEffect с пустым массивом зависимостей гарантирует
однократное создание картыremove() критичен для освобождения WebGL-контекстаВ режиме разработки React StrictMode может вызывать двойной монтаж компонентов. Это приводит к повторной инициализации карты, если не защищаться от повторного создания.
Типовая защита:
if (mapRef.current) return;
Альтернативный подход — использование флага инициализации:
const initialized = useRef(false);
useEffect(() => {
if (initialized.current) return;
initialized.current = true;
...
}, []);
Карты MapLibre являются императивными объектами, поэтому синхронизация состояния требует явного мостика.
useEffect(() => {
if (!mapRef.current) return;
mapRef.current.setCenter(center);
}, [center]);
useEffect(() => {
if (!mapRef.current) return;
mapRef.current.setZoom(zoom);
}, [zoom]);
MapLibre предоставляет событийную модель, которая интегрируется через
подписки внутри useEffect.
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const handleMove = () => {
const center = map.getCenter();
const zoom = map.getZoom();
console.log(center, zoom);
};
map.on("move", handleMove);
return () => {
map.off("move", handleMove);
};
}, []);
При необходимости синхронизации карты и состояния React используется контролируемая модель.
const [viewState, setViewState] = useState({
center: [0, 0],
zoom: 2
});
Подписка на события карты:
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const updateState = () => {
setViewState({
center: map.getCenter().toArray(),
zoom: map.getZoom()
});
};
map.on("moveend", updateState);
return () => map.off("moveend", updateState);
}, []);
И обратная синхронизация:
useEffect(() => {
if (!mapRef.current) return;
mapRef.current.setCenter(viewState.center);
mapRef.current.setZoom(viewState.zoom);
}, [viewState]);
Добавление данных выполняется после события load.
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const onL oad = () => {
map.addSource("points", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
map.addLayer({
id: "points-layer",
type: "circle",
source: "points",
paint: {
"circle-radius": 6,
"circle-color": "#ff0000"
}
});
};
map.on("load", onLoad);
return () => map.off("load", onLoad);
}, []);
useEffect(() => {
if (!mapRef.current) return;
const source = mapRef.current.getSource("points");
if (source) {
source.setData(geojson);
}
}, [geojson]);
Практика разделения:
Это снижает количество перерендеров и изолирует WebGL-логику.
const mapOptions = useMemo(() => ({
style: "https://demotiles.maplibre.org/style.json",
antialias: true
}), []);
Стабилизация зависимостей:
const center = useMemo(() => [30, 60], []);
MapLibre требует явного вызова resize() при изменении
размеров контейнера.
useEffect(() => {
if (!mapRef.current) return;
const resizeObserver = new ResizeObserver(() => {
mapRef.current.resize();
});
resizeObserver.observe(mapContainer.current);
return () => resizeObserver.disconnect();
}, []);
Типизация карты повышает стабильность интеграции.
import maplibregl from "maplibre-gl";
import { useRef } from "react";
const mapRef = useRef<maplibregl.Map | null>(null);
Типы событий:
const handleClick = (e: maplibregl.MapMouseEvent) => {
console.log(e.lngLat);
};
UI-оверлеи не должны влиять на WebGL-контекст.
return (
<div style={{ position: "relative" }}>
<div ref={mapContainer} style={{ height: "100vh" }} />
<div style={{ position: "absolute", top: 10, left: 10 }}>
Панель управления
</div>
</div>
);
useEffect(() => {
if (!mapRef.current) return;
const el = document.createElement("div");
el.className = "marker";
new maplibregl.Marker(el)
.setLngLat([30, 60])
.addTo(mapRef.current);
}, []);
DOM-узлы можно синхронизировать через refs и порталы, сохраняя React-управление UI, но размещая элементы в MapLibre.
movesetStatesetDataПри использовании Next.js или аналогичных SSR-систем карта должна создаваться только на клиенте.
useEffect(() => {
if (typeof window === "undefined") return;
if (!mapContainer.current) return;
mapRef.current = new maplibregl.Map({
container: mapContainer.current,
style: "https://demotiles.maplibre.org/style.json"
});
}, []);
Удаление карты критично для предотвращения утечек памяти:
return () => {
mapRef.current?.remove();
mapRef.current = null;
};
Особенно важно при:
В сложных приложениях карта превращается в сервисный слой:
React остаётся управляющим слоем состояния, тогда как MapLibre функционирует как независимый графический движок, взаимодействующий через строго определённые интерфейсы.