Как стать автором
Обновить

Хранение инстанса карты mapbox-gl вне React

Время на прочтение6 мин
Количество просмотров2.2K

В этом посте будет рассмотрен способ использования mapbox-gl в React приложении, с хранением инстанса карты во вспомогательном объекте обертке. Это позволяет обращаться к карте из любой части приложения, без необходимости передавать ссылку на карту средствами React

Под словами "ссылка на инстанс карты" или "ссылка на карту" подразумевается переменная содержащая объект карты.

Это обусловлено тем, что переменная содержащая объект на самом деле содержит ссылку на него, но не сам объект, тут можно узнать об этом подробнее https://blog.noveogroup.ru/2021/02/upravlenie-pamyatju-v-javascript/

Эта статья входит в цикл статей

Инструкцию по созданию нового проекта и имплементацию компонента карты, вы можете посмотреть в моем предыдущем посте

Классический подход

При использовании mapbox-gl в React приложении, возникает проблема организации доступа к нему из других компонентов приложения

Как правило ссылку на инстанс mapbox-gl можно передавать через props либо через контекст

Допустим наше приложение состоит из полноэкранного компонента с веб картой и боковой панели, содержащей элементы для взаимодействия с картой

В последующих примерах кода некоторые детали опущены, например стили

Пример передачи ссылки через props

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

components/pass-map-with-props.tsx

import * as React from "react";
import MapboxMap from "./mapbox-map";

export const Sidebar: React.FC<{ map: mapboxgl.Map | undefined }> = ({
  map,
}) => {
  return <div>...some sidebar content...</div>;
};

const App: React.FC = () => {
  const [map, setMap] = React.useState<mapboxgl.Map>();

  return (
    <div>
      <MapboxMap onLoaded={setMap} />
      <Sidebar map={map} />
    </div>
  );
};

export default App;

Ссылка на исходный код

Когда карта полностью загрузится, ссылка на нее сохраняется внутри React.useState, далее она передается в компонент Sidebar через props

Пример передачи ссылки через контекст

Чтобы передать ссылку на инстанс через контекст, создадим контекст и обернем в него Sidebar компонент, а так же хук useMapboxMap для доступа к инстансу карты через контекст

components/pass-map-with-context.tsx

import * as React from "react";
import MapboxMap from "./mapbox-map";

const MapboxMapContext = React.createContext<mapboxgl.Map | undefined>(
  undefined
);

function useMapboxMap() {
  return React.useContext(MapboxMapContext);
}

const Sidebar: React.FC = () => {
  const mapboxMap = useMapboxMap();

  return <div>...some sidebar content...</div>;
};

const App: React.FC = () => {
  const [map, setMap] = React.useState<mapboxgl.Map>();

  return (
    <div>
      <MapboxMap onLoaded={setMap} />
      <MapboxMapContext.Provider value={map}>
        <Sidebar />
      </MapboxMapContext.Provider>
    </div>
  );
};

export default App;

Ссылка на исходный код

Теперь на инстанс карты можно сослаться откуда угодно изнутри Sidebar используя useMapboxMap

В обоих примерах есть один общий недостаток, передача ссылки на карту в дочерние компоненты влечет за собой дополнительные неудобства при разработке, это может вызвать такие проблемы как prop drilling, а при использовании контекста отдельное внимание потребуется выделить организации иерархии компонентов. Если вы будете использовать его в ваших хуках, это может потребовать дополнительного кода для избежания проблем с exhaustive-deps. Так же могут возникать сложности при необходимости обращения к карте из внешнего хранилища состояния, например из Redux или XState.

Хранение вне React

Хотелось бы иметь возможность обращаться к карте из любого компонента, без необходимости как-то специально его туда передавать и указывать в списках зависимостей хуков

Вспомогательный объект

Для того чтобы иметь возможность сделать это потребуется вспомогательный объект-обертка в котором будет храниться ссылка на инстанс карты

lib/map-wrapper.ts

class MapWrapper {
  private _map?: mapboxgl.Map;

  set map(instance: mapboxgl.Map) {
    this._map = instance;
  }

  get map() {
    if (typeof this._map === "undefined")
      throw new Error("Cannot access mapbox map before inilizing it");
    return this._map;
  }

  remove() {
    if (typeof this._map === "undefined") 
            throw new Error("Cannot remove mapbox map before inilizing");
    this._map.remove();
    this._map = undefined;
  }
}

export const mapbox = new MapWrapper();

Ссылка на исходный код

Создадим класс MapWrapper используя возможности typescript для работы с классами

Инстанс карты будет храниться в приватной переменной _map, зададим также сеттер и геттер для этой переменной и метод для удаления инстанса карты

  • set - для сохранения карты в приватную переменную

  • get - если карта не инициализирована, при попытке обратиться к ней выбрасывается исключение

  • remove - если страница с картой например была закрыта, инстанс карты необходимо удалить чтобы избежать проблем с утечкой памяти, метод можно вызвать только если карта была инициализирована, в ином случае вызывается исключение

Применительно к предыдущим примерам получим следующее:

import * as React from "react";
import "mapbox-gl/dist/mapbox-gl.css";
import MapboxMap from "../components/mapbox-map";
// Импорт объекта обертки
import mapbox from "../lib/map-wrapper"

const Sidebar: React.FC = () => {
  const setCenterToMoscow = () => mapbox.map.setCenter([
    37.60345458984374,
    55.695776911386126
  ])

  return (
    <div>
	    <button onClick={setCenterToMoscow}>
    		Set map center to Moscow
    	</button>
    </div>
  );
};

const WithOutsideMap: React.FC = () => {
  const onMapCreated = React.useCallback((map: mapboxgl.Map) => {
    // сохраняем инстанс карты в обертку при его создании
    mapbox.map = map;
  }, []);

  return (
    <div>
      <MapboxMap onCreated={onMapCreated} />
	    <Sidebar />
    </div>
  );
};

export default WithOutsideMap;

Теперь мы можем обращаться к карте из компонента Sidebar, без необходимости его передавать в компонент средствами React.

Живой пример

Давайте воспроизведем пример из документации mapboxgl с отображением текущего центра и зума карты

components/with-outside-map.tsx

import * as React from "react";
import "mapbox-gl/dist/mapbox-gl.css";
import MapboxMap from "../components/mapbox-map";
import mapbox from "../lib/map-wrapper";

const WithOutsideMap: React.FC = () => {
    // текущий зум и центр карты
  const [viewport, setViewport] = React.useState({
    center: ["-74.5165", "40.0021"],
    zoom: "9.00",
  });

  const { center: [lng, lat], zoom } = viewport;

  const onMapCreated = React.useCallback((map: mapboxgl.Map) => {
    mapbox.map = map;
        // после того как карта создана, привяжем ивент, по событию "move"
        // обновляющий значение viewport
    mapbox.map.on("move", () => {
      setViewport({
        center: [
          mapbox.map.getCenter().lng.toFixed(4),
          mapbox.map.getCenter().lat.toFixed(4),
        ],
        zoom: mapbox.map.getZoom().toFixed(2),
      });
    });
  }, []);

  return (
    <div className="app-container">
      <div className="map-wrapper">
        <div className="viewport-panel">
          Longitude: {lng} | Latitude: {lat} | Zoom: {zoom}
        </div>
        <MapboxMap
          onCreated={onMapCreated}
          {/* зададим начальный центр и зум для карты */}
          initialOptions={{ center: [+lng, +lat], zoom: +zoom }}
        />
      </div>
    </div>
  );
};

export default WithOutsideMap;

Ссылка на исходный код

Центр карты будет храниться в объекте viewport, при передвижении карты пользователем, будет срабатывать событие move, по которому мы обновляем текущее значение viewport.

Значения хранящиеся во viewport используются для отображения текущих координат центра и зума карты, а так же для передачи параметров в initialOptions компонента карты.

Объект initialOptions используется единожды, при создании карты.

При передаче параметров в initialOptions можно заметить + перед переменными, это нужно для конвертации их в числа, так как мы храним цифровые значения как строки.

Ссылка на запущенное приложение

В следующей, завершающей, статье цикла я планирую рассказать об управлении состоянием React приложения сmapbox-gl с использованием XState

Ссылки на исходный код и запущенное приложение

Теги:
Хабы:
Рейтинг0
Комментарии6

Публикации

Истории

Работа

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань