React integration

Особенности совместного использования React и Mapbox GL JS

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

В React компоненты могут многократно перерисовываться, однако экземпляр карты должен создаваться только один раз. По этой причине для хранения карты обычно используются ссылки (useRef), а инициализация выполняется внутри эффекта (useEffect).

Типичная схема взаимодействия выглядит следующим образом:

  1. React создаёт контейнер DOM.
  2. После монтирования компонента запускается Mapbox GL JS.
  3. Экземпляр карты сохраняется в ref.
  4. Последующие изменения состояния React обновляют карту через API Mapbox.
  5. При размонтировании выполняется очистка ресурсов.

Установка зависимостей

Установка библиотеки выполняется через npm:

npm install mapbox-gl

или через Yarn:

yarn add mapbox-gl

Дополнительно необходимо подключить стили:

import 'mapbox-gl/dist/mapbox-gl.css';

Базовый React-компонент карты

Наиболее распространённый вариант интеграции основан на функциональных компонентах и хуках.

import { useEffect, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

mapboxgl.accessToken = 'YOUR_TOKEN';

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

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

        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={{
                width: '100%',
                height: '500px'
            }}
        />
    );
}

export default Map;

Назначение ссылок

В примере используются две ссылки:

const mapContainer = useRef(null);
const map = useRef(null);

Первая хранит DOM-элемент контейнера.

Вторая хранит экземпляр объекта Map, созданного библиотекой.

Без использования useRef карта пересоздавалась бы при каждом рендере компонента.


Работа с жизненным циклом компонента

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

Создание выполняется внутри эффекта:

useEffect(() => {
    map.current = new mapboxgl.Map({...});
}, []);

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

Удаление карты

Mapbox создаёт большое количество внутренних ресурсов:

  • WebGL-контекст;
  • обработчики событий;
  • источники данных;
  • слои;
  • таймеры.

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

map.current.remove();

Это предотвращает утечки памяти.


Использование состояния React

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

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

const [center, setCenter] = useState([37.6176, 55.7558]);

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

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

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

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


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

Нередко требуется обратная связь — карта изменяет состояние React.

Например, отслеживание текущего центра:

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

    setLng(center.lng.toFixed(4));
    setLat(center.lat.toFixed(4));
});

Полный пример:

const [lng, setLng] = useState(0);
const [lat, setLat] = useState(0);
const [zoom, setZoom] = useState(0);

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

        setLng(center.lng.toFixed(4));
        setLat(center.lat.toFixed(4));
        setZoom(map.current.getZoom().toFixed(2));
    });
}, []);

Отображение информации:

<div>
    Longitude: {lng}
    Latitude: {lat}
    Zoom: {zoom}
</div>

Использование useMemo для конфигурации

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

const mapOptions = useMemo(() => ({
    style: 'mapbox://styles/mapbox/light-v11',
    center: [30.3158, 59.9398],
    zoom: 11
}), []);

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

map.current = new mapboxgl.Map({
    container: mapContainer.current,
    ...mapOptions
});

Это предотвращает создание новых объектов конфигурации при каждом рендере.


Создание пользовательского хука

Повторяющаяся логика может быть вынесена в собственный хук.

Реализация

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

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

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

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

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

    return {
        containerRef,
        mapRef
    };
}

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

const { containerRef, mapRef } = useMapbox({
    style: 'mapbox://styles/mapbox/dark-v11',
    center: [0, 0],
    zoom: 2
});
return <div ref={containerRef} />;

Такой подход повышает переиспользуемость кода.


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

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

map.current.on('load', () => {
    new mapboxgl.Marker()
        .setLngLat([37.6176, 55.7558])
        .addTo(map.current);
});

Управление маркерами через React

Массив объектов:

const markers = [
    {
        id: 1,
        lng: 37.6176,
        lat: 55.7558
    },
    {
        id: 2,
        lng: 30.3158,
        lat: 59.9398
    }
];

Создание:

markers.forEach(marker => {
    new mapboxgl.Marker()
        .setLngLat([marker.lng, marker.lat])
        .addTo(map.current);
});

Рендеринг маркеров из состояния

const [locations, setLocations] = useState([]);

После получения данных:

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

    locations.forEach(location => {
        new mapboxgl.Marker()
            .setLngLat([
                location.lng,
                location.lat
            ])
            .addTo(map.current);
    });
}, [locations]);

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


Использование GeoJSON вместе с React

React часто получает данные через API и передаёт их карте.

Источник данных

const geojson = {
    type: 'FeatureCollection',
    features: [
        {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [
                    37.6176,
                    55.7558
                ]
            }
        }
    ]
};

Добавление источника

map.current.addSource('points', {
    type: 'geojson',
    data: geojson
});

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

map.current.addLayer({
    id: 'points-layer',
    type: 'circle',
    source: 'points',
    paint: {
        'circle-radius': 8,
        'circle-color': '#ff0000'
    }
});

Обновление GeoJSON при изменении состояния

const [data, setData] = useState(initialGeoJson);

Обновление источника:

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

    const source = map.current.getSource('points');

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

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


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

Событие клика

map.current.on('click', event => {
    console.log(event.lngLat);
});

Событие загрузки

map.current.on('load', () => {
    console.log('Map loaded');
});

Событие изменения масштаба

map.current.on('zoom', () => {
    console.log(map.current.getZoom());
});

Подписка и отписка от событий

Для корректной работы React желательно сохранять обработчики.

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

    map.current.on('move', handleMove);

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

Такой подход предотвращает накопление лишних обработчиков.


Работа с асинхронными данными

Получение объектов с сервера:

useEffect(() => {
    async function loadData() {
        const response = await fetch('/api/points');
        const data = await response.json();

        setGeoJson(data);
    }

    loadData();
}, []);

После обновления состояния источник карты автоматически получает новые данные через setData().


Интеграция с Context API

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

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

import { createContext } from 'react';

export const MapContext = createContext(null);

Провайдер:

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

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

const mapRef = useContext(MapContext);

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


React Router и Mapbox

При использовании маршрутизации карта может:

  • сохраняться между страницами;
  • уничтожаться при смене маршрута;
  • загружаться только для определённых экранов.

Пример ленивой загрузки:

const MapPage = lazy(() => import('./MapPage'));

Это уменьшает размер первоначального JavaScript-бандла.


Серверный рендеринг

Mapbox GL JS зависит от браузерных API:

  • WebGL;
  • window;
  • document.

Поэтому при использовании SSR требуется создавать карту только на клиенте.

Проверка:

if (typeof window !== 'undefined') {
    // создание карты
}

Для Next.js часто используется динамический импорт:

import dynamic from 'next/dynamic';

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

Оптимизация производительности

Не хранить карту в состоянии

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

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

Правильно:

const mapRef = useRef(null);

Объект карты не участвует в процессе рендеринга интерфейса.


Минимизировать перерисовки

Избегать обновления состояния при каждом пикселе перемещения карты.

Плохо:

map.on('move', () => {
    setCenter(map.getCenter());
});

Лучше:

map.on('moveend', () => {
    setCenter(map.getCenter());
});

Использовать React.memo

Для тяжёлых дочерних компонентов:

export default React.memo(ControlPanel);

Это уменьшает количество лишних обновлений интерфейса.


Типовая архитектура React-приложения с Mapbox

Пример структуры проекта:

src/
├── components/
│   ├── Map/
│   │   ├── Map.jsx
│   │   ├── MarkerLayer.jsx
│   │   ├── RouteLayer.jsx
│   │   └── Controls.jsx
│
├── hooks/
│   ├── useMapbox.js
│   └── useGeoJson.js
│
├── context/
│   └── MapContext.js
│
├── pages/
│   └── Dashboard.jsx
│
└── services/
    └── api.js

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