Связка Leaflet и React требует согласования двух разных моделей работы с DOM: Leaflet напрямую управляет DOM-узлами карты, тогда как React строит виртуальное дерево и обновляет его декларативно. Основная сложность заключается в предотвращении конфликтов между этими подходами и корректном управлении жизненным циклом карты.
При использовании Leaflet в React ключевой принцип заключается в том, что контейнер карты создаётся React, но дальнейшее управление этим контейнером полностью передаётся Leaflet.
Типичная структура:
import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
export default function Map() {
const mapRef = useRef(null);
useEffect(() => {
const map = L.map(mapRef.current).setView([51.505, -0.09], 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors"
}).addTo(map);
return () => {
map.remove();
};
}, []);
return <div ref={mapRef} style={{ height: "400px" }} />;
}
Ключевое правило: контейнер карты должен существовать до инициализации Leaflet, иначе возникает ошибка привязки к DOM.
React может повторно вызывать рендер компонента, но Leaflet-карта
должна создаваться строго один раз. Для этого используется
useRef как хранилище экземпляра карты.
const mapInstanceRef = useRef(null);
useEffect(() => {
if (mapInstanceRef.current) return;
mapInstanceRef.current = L.map(mapRef.current);
}, []);
Такой подход предотвращает повторную инициализацию и утечки памяти.
Leaflet создаёт слушатели событий, DOM-слои и внутренние структуры. Без явного удаления это приводит к накоплению памяти.
return () => {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
};
Маркер в Leaflet — императивная сущность, поэтому React-данные требуют синхронизации через эффекты.
useEffect(() => {
if (!mapInstanceRef.current) return;
const marker = L.marker([51.505, -0.09])
.addTo(mapInstanceRef.current)
.bindPopup("Точка интереса");
return () => {
marker.remove();
};
}, []);
При изменении props координаты должны обновлять состояние Leaflet-объектов:
useEffect(() => {
if (!mapInstanceRef.current) return;
marker.setLatLng(position);
}, [position]);
Leaflet использует слои как базовую абстракцию. В React важно хранить ссылки на слои, чтобы корректно обновлять их при изменении данных.
const geoJsonLayerRef = useRef(null);
useEffect(() => {
if (!mapInstanceRef.current) return;
geoJsonLayerRef.current = L.geoJSON(data).addTo(mapInstanceRef.current);
return () => {
geoJsonLayerRef.current.remove();
};
}, [data]);
При изменении GeoJSON предпочтительно не пересоздавать слой, а обновлять:
geoJsonLayerRef.current.clearLayers();
geoJsonLayerRef.current.addData(newData);
Leaflet использует собственную систему событий. В React важно подписываться и отписываться вручную.
useEffect(() => {
const map = mapInstanceRef.current;
if (!map) return;
const handleClick = (e) => {
console.log(e.latlng);
};
map.on("click", handleClick);
return () => {
map.off("click", handleClick);
};
}, []);
Игнорирование отписки приводит к накоплению обработчиков и дублированию логики.
Библиотека react-leaflet предоставляет декларативную обёртку над Leaflet, синхронизируя React-дерево с картой.
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
export default function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: "400px" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker position={[51.505, -0.09]}>
<Popup>Точка интереса</Popup>
</Marker>
</MapContainer>
);
}
Здесь управление жизненным циклом карты абстрагировано, но внутренние принципы Leaflet остаются неизменными.
Для доступа к API карты используется useMap:
import { useMap } from "react-leaflet";
import { useEffect } from "react";
function MapController() {
const map = useMap();
useEffect(() => {
map.setView([40, 40], 5);
}, [map]);
return null;
}
Такой подход отделяет управление состоянием карты от визуальных компонентов.
Частая ошибка — пересоздание GeoJSON или маркеров при каждом рендере React-компонента. Это приводит к деградации производительности.
Оптимизация:
useRefПри большом количестве точек используется кластеризация через leaflet.markercluster.
import "leaflet.markercluster";
const markers = L.markerClusterGroup();
data.forEach((item) => {
markers.addLayer(L.marker(item.coords));
});
map.addLayer(markers);
В React важно хранить markers в ref и обновлять только
при изменении данных.
React-компоненты карты должны быть изолированы от глобального состояния приложения. Любое изменение состояния верхнего уровня может привести к пересозданию карты при неправильной архитектуре.
Практика:
Leaflet позволяет создавать собственные UI-контролы, которые можно интегрировать в React через imperative API.
const customControl = L.control({ position: "topright" });
customControl.on Add = function () {
const div = L.DomUtil.create("div", "custom-control");
div.innerHTML = "Контроль";
return div;
};
customControl.addTo(map);
При интеграции с React такие элементы должны управляться через эффекты и удаляться при размонтировании.
Leaflet поддерживает множество источников тайлов. В React важно динамически менять URL слоя.
const tileLayerRef = useRef(null);
useEffect(() => {
if (!mapInstanceRef.current) return;
tileLayerRef.current = L.tileLayer(url).addTo(mapInstanceRef.current);
return () => {
tileLayerRef.current.remove();
};
}, [url]);
Leaflet зависит от браузерного DOM, поэтому при серверном рендеринге
возникают ошибки window is not defined.
Решение заключается в ленивой загрузке:
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./Map"), { ssr: false });
Также важно избегать инициализации Leaflet вне
useEffect.
Основная архитектурная задача — поддержание синхронизации между:
Подход:
useEffectuseEffect(() => {
if (!map) return;
map.setZoom(zoom);
}, [zoom]);
Причина: отсутствие useRef или неправильные зависимости
useEffect.
Причина: отсутствие map.remove() и
off().
Причина: попытка инициализации до монтирования контейнера.
Причина: одновременное управление DOM одними и теми же элементами.
Leaflet остаётся императивным слоем, React — декларативным.
Каждый слой карты управляется отдельным React-компонентом, который отвечает только за свой жизненный цикл.
Использование React Context для передачи экземпляра карты вниз по дереву без проп-дриллинга.
const MapContext = createContext(null);
Это позволяет подключать слои и контроллеры без жесткой связности компонентов.