React интеграция

Интеграция MapLibre GL JS в React строится вокруг императивного API карты внутри декларативной модели UI. Карта остаётся внешним объектом, управляемым через жизненный цикл компонентов, refs и эффекты, тогда как React отвечает за конфигурацию, состояние и синхронизацию событий.

Ключевая особенность подхода — разделение ответственности:

  • React управляет состоянием приложения, параметрами и UI-логикой
  • MapLibre GL JS управляет WebGL-контекстом, рендерингом и интерактивной картой
  • Связующим слоем выступают эффекты (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

В режиме разработки React StrictMode может вызывать двойной монтаж компонентов. Это приводит к повторной инициализации карты, если не защищаться от повторного создания.

Типовая защита:

if (mapRef.current) return;

Альтернативный подход — использование флага инициализации:

const initialized = useRef(false);

useEffect(() => {
  if (initialized.current) return;
  initialized.current = true;
  ...
}, []);

Управление состоянием карты через React

Карты MapLibre являются императивными объектами, поэтому синхронизация состояния требует явного мостика.

Центр и масштаб

useEffect(() => {
  if (!mapRef.current) return;

  mapRef.current.setCenter(center);
}, [center]);

useEffect(() => {
  if (!mapRef.current) return;

  mapRef.current.setZoom(zoom);
}, [zoom]);

Принцип синхронизации

  • React state → MapLibre API
  • избегается обратное управление без явной необходимости
  • каждое изменение параметра изолировано в отдельный эффект

Обработка событий карты

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 state без необходимости
  • высокая частота событий (move, zoom) требует throttling

Интеграция с React state (двусторонняя модель)

При необходимости синхронизации карты и состояния 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]);

Оптимизация рендеринга в React

Разделение ответственности компонентов

Практика разделения:

  • MapContainer — создание карты
  • Layers — управление слоями
  • Controls — UI элементы поверх карты

Это снижает количество перерендеров и изолирует WebGL-логику.


useMemo для конфигураций

const mapOptions = useMemo(() => ({
  style: "https://demotiles.maplibre.org/style.json",
  antialias: true
}), []);

Избежание лишних эффектов

Стабилизация зависимостей:

const center = useMemo(() => [30, 60], []);

Resize и адаптивность

MapLibre требует явного вызова resize() при изменении размеров контейнера.

useEffect(() => {
  if (!mapRef.current) return;

  const resizeObserver = new ResizeObserver(() => {
    mapRef.current.resize();
  });

  resizeObserver.observe(mapContainer.current);

  return () => resizeObserver.disconnect();
}, []);

Использование TypeScript

Типизация карты повышает стабильность интеграции.

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

React-компоненты поверх карты

UI-оверлеи не должны влиять на WebGL-контекст.

return (
  <div style={{ position: "relative" }}>
    <div ref={mapContainer} style={{ height: "100vh" }} />
    <div style={{ position: "absolute", top: 10, left: 10 }}>
      Панель управления
    </div>
  </div>
);

Интеграция кастомных маркеров

DOM-маркеры

useEffect(() => {
  if (!mapRef.current) return;

  const el = document.createElement("div");
  el.className = "marker";

  new maplibregl.Marker(el)
    .setLngLat([30, 60])
    .addTo(mapRef.current);
}, []);

React-маркеры через портал-подход

DOM-узлы можно синхронизировать через refs и порталы, сохраняя React-управление UI, но размещая элементы в MapLibre.


Проблемы производительности и их источники

Частые причины деградации:

  • неконтролируемые события move
  • постоянные вызовы setState
  • пересоздание карты
  • частые обновления источников GeoJSON
  • отсутствие memoization конфигураций

Подход к стабилизации:

  • throttling событий карты
  • разнесение state и map-instance
  • минимизация setData
  • контроль жизненного цикла через refs

SSR и клиентская инициализация

При использовании 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"
  });
}, []);

Очистка ресурсов WebGL

Удаление карты критично для предотвращения утечек памяти:

return () => {
  mapRef.current?.remove();
  mapRef.current = null;
};

Особенно важно при:

  • переходах между страницами
  • условном рендеринге компонентов
  • модальных окнах с картой

Масштабируемая архитектура интеграции

В сложных приложениях карта превращается в сервисный слой:

  • MapProvider (контекст)
  • MapInstanceService (инкапсуляция API)
  • LayerManager (слои)
  • SourceManager (данные)
  • EventBridge (события)

React остаётся управляющим слоем состояния, тогда как MapLibre функционирует как независимый графический движок, взаимодействующий через строго определённые интерфейсы.