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

Kate

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

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

Управление состоянием mapbox-gl в React
Описание проблемыВ процессе моей работы в geoalert.io я не раз сталкивался с проблемой управления со...
habr.com
Инструкцию по созданию нового проекта и имплементацию компонента карты, вы можете посмотреть в моем предыдущем посте

Использование mapbox-gl в React и Next.js
ВведениеВ данной статье я хочу описать известные мне способы встраивания mapbox-gl в React приложени...
habr.com

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

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

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

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

3d873bfd1a69c3a4496cf1a387d7217c.png

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

Пример передачи ссылки через 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, зададим также сеттер и геттер для этой переменной, вспомогательный метод created и метод для удаления инстанса карты

  • 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 с отображением текущего центра и зума карты


Use Mapbox GL JS in a React app | Help
docs.mapbox.com
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 можно заметить + перед переменными, это нужно для конвертации их в числа, так как мы храним цифровые значения как строки.

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

8a259266c7e848ea87bf2093d744f977.gif

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

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


GitHub - dqunbp/using-mapbox-gl-with-react: This is a repository for an example of using React with mapbox-gl and Next.js. You can use it as a new project template. Read more about this project from my blog post, link in README.md
github.com


using-mapbox-gl-with-react.vercel.app

Источник статьи: https://habr.com/ru/post/569302/
 
Сверху