React hooks для карты

Интеграция картографического движка MapLibre GL JS в приложения на React требует строгого разделения ответственности между жизненным циклом карты и жизненным циклом компонентов интерфейса. Основная сложность заключается в том, что карта представляет собой императивный объект, тогда как React оперирует декларативной моделью обновлений.

Правильная организация hooks позволяет:

  • изолировать создание и уничтожение карты
  • синхронизировать состояние React и состояние карты
  • избегать утечек памяти и повторной инициализации WebGL-контекста
  • управлять слоями и источниками данных как реактивными сущностями

Базовый хук и инициализация MapLibre-карты

Фундаментальный подход заключается в создании 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]);
}

Важно учитывать, что слой всегда зависит от существующего источника, поэтому порядок инициализации критичен.


Координация событий карты через hooks

События 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). Основная проблема — предотвращение циклических обновлений.

Решение заключается в разделении источника истины:

  • React хранит желаемое состояние
  • MapLibre хранит фактическое состояние отображения
  • синхронизация происходит через эффекты с защитой от повторных вызовов
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 критически важно избегать пересоздания объектов:

  • style objects должны мемоизироваться через useMemo
  • обработчики событий — через useCallback
  • конфигурации слоёв — через стабильные ссылки
const 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 в единую карту

Полноценная архитектура строится через композицию специализированных 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 через дерево.


Управление разрушением и предотвращение утечек

Особое внимание требуется очистке ресурсов:

  • удаление event listeners
  • уничтожение источников и слоёв
  • вызов map.remove()

Нарушение этих правил приводит к накоплению WebGL-контекстов и деградации производительности.


Работа с асинхронной загрузкой стилей

MapLibre поддерживает динамическую загрузку стилей, что требует отдельного контроля:

useEffect(() => {
  if (!map) return;

  map.setStyle(styleUrl);
}, [map, styleUrl]);

При смене стиля необходимо учитывать, что слои и источники удаляются, поэтому их восстановление должно быть реактивным после события styledata.


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

Кастомные UI-элементы карты удобно инкапсулировать:

export function useMapControl(map, control) {
  useEffect(() => {
    if (!map || !control) return;

    map.addControl(control);

    return () => {
      map.removeControl(control);
    };
  }, [map, control]);
}

Это обеспечивает единый жизненный цикл между React-компонентами и контролами MapLibre.


Реактивная модель карты в React-архитектуре

В результате применения hooks карта перестаёт быть «внешним объектом» и становится управляемой частью состояния приложения. Однако её поведение остаётся императивным внутри, что требует строгой дисциплины:

  • отсутствие прямого доступа к DOM карты вне hooks
  • изоляция side-effects
  • централизованное управление источниками и слоями
  • стабильные зависимости эффектов

Такой подход позволяет масштабировать картографические интерфейсы от простых визуализаций до сложных геоинформационных систем без потери управляемости.