В этом посте будет рассмотрен способ использования mapbox-gl в React приложении, с хранением инстанса карты во вспомогательном объекте обертке. Это позволяет обращаться к карте из любой части приложения, без необходимости передавать ссылку на карту средствами React
Управление состоянием mapbox-gl в React
Описание проблемыВ процессе моей работы в geoalert.io я не раз сталкивался с проблемой управления со...
habr.com
Использование mapbox-gl в React и Next.js
ВведениеВ данной статье я хочу описать известные мне способы встраивания mapbox-gl в React приложени...
habr.com
Как правило ссылку на инстанс mapbox-gl можно передавать через 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
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.
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 и метод для удаления инстанса карты
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.
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 можно заметить + перед переменными, это нужно для конвертации их в числа, так как мы храним цифровые значения как строки.
Ссылка на запущенное приложение
В следующей, завершающей, статье цикла я планирую рассказать об управлении состоянием 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/
Под словами "ссылка на инстанс карты" или "ссылка на карту" подразумевается переменная содержащая объект карты.
Это обусловлено тем, что переменная содержащая объект на самом деле содержит ссылку на него, но не сам объект, тут можно узнать об этом подробнее 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 либо через контекст
Допустим наше приложение состоит из полноэкранного компонента с веб картой и боковой панели, содержащей элементы для взаимодействия с картой
В последующих примерах кода некоторые детали опущены, например стили
Пример передачи ссылки через 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 можно заметить + перед переменными, это нужно для конвертации их в числа, так как мы храним цифровые значения как строки.
Ссылка на запущенное приложение
В следующей, завершающей, статье цикла я планирую рассказать об управлении состоянием 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/