React hooks для карты

Mapbox GL JS управляет картой через собственный жизненный цикл и напрямую взаимодействует с DOM-элементом контейнера. React, напротив, использует декларативную модель рендеринга и виртуальный DOM. По этой причине интеграция карты в React почти всегда строится вокруг хуков, которые позволяют контролировать создание, обновление и уничтожение экземпляра карты.

Основная задача React hooks при работе с Mapbox GL JS — корректно синхронизировать состояние приложения и состояние карты без лишних перерисовок и утечек памяти.

Наиболее часто используются следующие хуки:

  • useRef
  • useEffect
  • useState
  • useMemo
  • useCallback

Использование useRef для хранения карты

Экземпляр карты не должен пересоздаваться при каждом рендере компонента. Для хранения ссылки на карту применяется useRef.

import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';

function Map() {
    const mapContainer = useRef(null);
    const map = useRef(null);

    useEffect(() => {
        map.current = new mapboxgl.Map({
            container: mapContainer.current,
            style: 'mapbox://styles/mapbox/streets-v12',
            center: [37.6176, 55.7558],
            zoom: 10
        });

        return () => {
            map.current.remove();
        };
    }, []);

    return <div ref={mapContainer} style={{ height: '500px' }} />;
}

В данном примере:

  • mapContainer хранит DOM-узел контейнера.
  • map хранит экземпляр карты.
  • карта создаётся только один раз после монтирования компонента.

Создание карты через useEffect

Хук useEffect является основным инструментом управления жизненным циклом Mapbox GL JS.

Инициализация карты выполняется после появления контейнера в DOM:

useEffect(() => {
    const mapInstance = new mapboxgl.Map({
        container: mapContainer.current,
        style: 'mapbox://styles/mapbox/light-v11'
    });

    return () => {
        mapInstance.remove();
    };
}, []);

Пустой массив зависимостей гарантирует однократное выполнение эффекта.

Если не удалить карту через remove(), останутся:

  • обработчики событий;
  • WebGL-контекст;
  • внутренние таймеры;
  • ссылки на DOM-элементы.

Это может привести к утечкам памяти.


Хранение состояния карты через useState

Некоторые параметры карты удобно сохранять в состоянии React.

Пример хранения координат центра:

const [center, setCenter] = useState({
    lng: 37.6176,
    lat: 55.7558
});

После перемещения карты состояние обновляется:

useEffect(() => {
    map.current.on('move', () => {
        const center = map.current.getCenter();

        setCenter({
            lng: center.lng,
            lat: center.lat
        });
    });
}, []);

Отображение координат:

<div>
    {center.lng.toFixed(4)}, {center.lat.toFixed(4)}
</div>

Таким образом React-компоненты получают актуальные данные о положении карты.


Синхронизация масштаба

Масштаб часто используется в интерфейсе для отображения текущего уровня приближения.

const [zoom, setZoom] = useState(10);

Обновление значения:

useEffect(() => {
    map.current.on('zoom', () => {
        setZoom(map.current.getZoom());
    });
}, []);

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

<p>Zoom: {zoom.toFixed(2)}</p>

Состояние React автоматически отражает изменения карты.


Отслеживание перемещений карты

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

const [bounds, setBounds] = useState(null);

useEffect(() => {
    map.current.on('moveend', () => {
        const currentBounds = map.current.getBounds();

        setBounds({
            north: currentBounds.getNorth(),
            south: currentBounds.getSouth(),
            east: currentBounds.getEast(),
            west: currentBounds.getWest()
        });
    });
}, []);

Полученные данные используются для:

  • загрузки объектов в видимой области;
  • фильтрации данных;
  • кластеризации;
  • серверных запросов.

Использование useCallback для обработчиков

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

Без оптимизации:

const handleMove = () => {
    console.log('move');
};

С оптимизацией:

const handleMove = useCallback(() => {
    console.log('move');
}, []);

Подключение:

useEffect(() => {
    map.current.on('move', handleMove);

    return () => {
        map.current.off('move', handleMove);
    };
}, [handleMove]);

Преимущества:

  • стабильная ссылка на функцию;
  • корректное удаление обработчиков;
  • меньше лишних операций.

Использование useMemo для вычисляемых данных

При большом количестве геоданных могут выполняться дорогостоящие вычисления.

Например, формирование GeoJSON:

const geojson = useMemo(() => {
    return {
        type: 'FeatureCollection',
        features: points.map(point => ({
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [point.lng, point.lat]
            }
        }))
    };
}, [points]);

GeoJSON будет пересчитываться только при изменении массива points.


Управление источниками данных через хуки

Добавление источника выполняется после загрузки стиля.

useEffect(() => {
    map.current.on('load', () => {
        map.current.addSource('cities', {
            type: 'geojson',
            data: geojson
        });
    });
}, [geojson]);

Обновление данных:

useEffect(() => {
    const source = map.current.getSource('cities');

    if (source) {
        source.setData(geojson);
    }
}, [geojson]);

Такой подход позволяет использовать React как источник данных для карты.


Управление слоями через useEffect

Добавление слоя:

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

    const addLayer = () => {
        map.current.addLayer({
            id: 'cities-layer',
            type: 'circle',
            source: 'cities',
            paint: {
                'circle-radius': 6,
                'circle-color': '#ff0000'
            }
        });
    };

    map.current.on('load', addLayer);

    return () => {
        if (map.current.getLayer('cities-layer')) {
            map.current.removeLayer('cities-layer');
        }
    };
}, []);

React контролирует жизненный цикл слоя аналогично жизненному циклу компонентов.


Собственный хук useMap

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

Пример:

import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';

export function useMap(options) {
    const containerRef = useRef(null);
    const mapRef = useRef(null);

    useEffect(() => {
        mapRef.current = new mapboxgl.Map({
            container: containerRef.current,
            ...options
        });

        return () => {
            mapRef.current.remove();
        };
    }, []);

    return {
        containerRef,
        mapRef
    };
}

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

function MapComponent() {
    const { containerRef } = useMap({
        style: 'mapbox://styles/mapbox/streets-v12',
        center: [37.6176, 55.7558],
        zoom: 10
    });

    return (
        <div
            ref={containerRef}
            style={{ height: '500px' }}
        />
    );
}

Преимущества:

  • повторное использование логики;
  • единообразная архитектура;
  • уменьшение объёма компонентов.

Собственный хук useMapCenter

Специализированный хук может отслеживать только центр карты.

function useMapCenter(mapRef) {
    const [center, setCenter] = useState(null);

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

        const updateCenter = () => {
            const center = mapRef.current.getCenter();

            setCenter({
                lng: center.lng,
                lat: center.lat
            });
        };

        mapRef.current.on('move', updateCenter);

        return () => {
            mapRef.current.off('move', updateCenter);
        };
    }, [mapRef]);

    return center;
}

Подобная декомпозиция делает код значительно более читаемым.


Работа с маркерами через хуки

Создание маркера:

const markerRef = useRef(null);

useEffect(() => {
    markerRef.current = new mapboxgl.Marker()
        .setLngLat([37.6176, 55.7558])
        .addTo(map.current);

    return () => {
        markerRef.current.remove();
    };
}, []);

Обновление координат:

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

    markerRef.current.setLngLat([
        longitude,
        latitude
    ]);
}, [longitude, latitude]);

React управляет состоянием маркера через свойства компонента.


Реакция на изменение props

Часто координаты приходят извне.

function Map({ center }) {
    // ...
}

Синхронизация:

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

    map.current.flyTo({
        center
    });
}, [center]);

Каждое изменение свойства вызывает плавное перемещение карты.


Использование useEffect для flyTo

Переход к новой точке:

useEffect(() => {
    map.current.flyTo({
        center: destination,
        zoom: 14,
        duration: 2000
    });
}, [destination]);

Это распространённый паттерн для:

  • поиска адресов;
  • навигации;
  • выбора объектов на карте;
  • работы со списками локаций.

Избежание лишних перерисовок

Распространённая ошибка — хранить экземпляр карты в состоянии.

Неправильно:

const [map, setMap] = useState(null);

Правильно:

const mapRef = useRef(null);

Причина проста: изменение состояния вызывает рендеринг компонента, тогда как изменение ref происходит без повторного рендера.

Для объектов Mapbox GL JS рекомендуется использовать именно useRef.


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

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

useEffect(() => {
    const handleClick = event => {
        console.log(event.lngLat);
    };

    map.current.on('click', handleClick);

    return () => {
        map.current.off('click', handleClick);
    };
}, []);

Такой подход предотвращает:

  • дублирование обработчиков;
  • утечки памяти;
  • ошибки после повторного монтирования компонента.

Работа с React Context

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

Создание контекста:

import { createContext } from 'react';

export const MapContext = createContext(null);

Провайдер:

<MapContext.Provider value={mapRef}>
    {children}
</MapContext.Provider>

Получение карты:

const mapRef = useContext(MapContext);

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


Архитектура хуков для крупных картографических приложений

В крупных проектах обычно формируется набор специализированных хуков:

useMap()
useMapCenter()
useMapBounds()
useMapZoom()
useMapMarkers()
useMapLayers()
useMapSources()
useMapEvents()
useMapControls()

Каждый хук отвечает за отдельную часть функциональности:

  • создание карты;
  • управление слоями;
  • управление источниками данных;
  • отслеживание событий;
  • контроль камеры;
  • работа с маркерами;
  • кластеризация объектов.

Подобная архитектура обеспечивает высокую масштабируемость приложения, упрощает тестирование и делает интеграцию Mapbox GL JS с React предсказуемой и удобной для сопровождения даже в проектах с большим количеством картографических компонентов и сложной бизнес-логикой.