React

Связка Leaflet и React требует согласования двух разных моделей работы с DOM: Leaflet напрямую управляет DOM-узлами карты, тогда как React строит виртуальное дерево и обновляет его декларативно. Основная сложность заключается в предотвращении конфликтов между этими подходами и корректном управлении жизненным циклом карты.

Базовая архитектура интеграции

При использовании Leaflet в React ключевой принцип заключается в том, что контейнер карты создаётся React, но дальнейшее управление этим контейнером полностью передаётся Leaflet.

Типичная структура:

  • React отвечает за создание DOM-элемента
  • 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;
};

Работа с маркерами в React

Маркер в 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

Библиотека 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 остаются неизменными.


Контроль экземпляра карты в react-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-компоненты карты должны быть изолированы от глобального состояния приложения. Любое изменение состояния верхнего уровня может привести к пересозданию карты при неправильной архитектуре.

Практика:

  • выделение Map-компонента
  • использование контекстов только для данных, не для экземпляра карты
  • мемоизация пропсов

Работа с кастомными контролами

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]);

SSR и Next.js ограничения

Leaflet зависит от браузерного DOM, поэтому при серверном рендеринге возникают ошибки window is not defined.

Решение заключается в ленивой загрузке:

import dynamic from "next/dynamic";

const Map = dynamic(() => import("./Map"), { ssr: false });

Также важно избегать инициализации Leaflet вне useEffect.


Синхронизация состояния React и карты

Основная архитектурная задача — поддержание синхронизации между:

  • состоянием React (props/state)
  • состоянием Leaflet (карта, слои, маркеры)

Подход:

  • React хранит только данные
  • Leaflet хранит визуальное представление
  • синхронизация выполняется через useEffect
useEffect(() => {
  if (!map) return;

  map.setZoom(zoom);
}, [zoom]);

Типичные проблемы интеграции

Повторная инициализация карты

Причина: отсутствие useRef или неправильные зависимости useEffect.

Утечки памяти

Причина: отсутствие map.remove() и off().

Несовпадение DOM

Причина: попытка инициализации до монтирования контейнера.

Конфликты React и Leaflet

Причина: одновременное управление DOM одними и теми же элементами.


Архитектурные паттерны

Imperative Bridge

Leaflet остаётся императивным слоем, React — декларативным.

  • React: данные и UI
  • Leaflet: рендер карты
  • bridge-компоненты: синхронизация

Layer Controller

Каждый слой карты управляется отдельным React-компонентом, который отвечает только за свой жизненный цикл.

Map Context Pattern

Использование React Context для передачи экземпляра карты вниз по дереву без проп-дриллинга.

const MapContext = createContext(null);

Это позволяет подключать слои и контроллеры без жесткой связности компонентов.