react-map-glを使ってみる
はじめに
Mapbox GL JSは強力なJavaScript用の地図ライブラリですが、公式にはReact用のライブラリはありません。ドキュメントにしたがってReactと組み合わせて使用することはできますが、Reactっぽいコードから逸脱している感は否めません。
そこでMapbox GL JSをラップしてReact対応したライブラリを探してみるといくつか見つかります。中でもreact-map-glがおすすめです。このライブラリはdeck.glのコンポーネントとして開発されてきたようですが、単独でも使用可能です。
react-map-glはMapboxが直接開発しているわけではありませんが、開発のサポートをしています。そういう点でも、信頼できるライブラリだと言えます。
プロジェクトの作成と依存関係のインストール
この記事ではvite + Reactを使用します。vite 公式ガイドにしたがってプロジェクトを作成します。
% npm create vite@latest
✔ Project name: … react-map-gl-sample
✔ Select a framework: › React
✔ Select a variant: › JavaScript
Scaffolding project in /Users/yochi/Downloads/20240715/react-map-gl-sample...
Done. Now run:
cd react-map-gl-sample
npm install
npm run dev
指示通りに依存関係をインストールします。
% cd react-map-gl-sample
% npm install
次にreact-map-glのGet Startedにしたがい、依存関係をインストールします。JavaScriptで開発する際には最後の@types/mapbox-gl
は不要です。
% npm install --save react-map-gl mapbox-gl @types/mapbox-gl
以下で実行します。
% npm run dev
地図を表示する
App.js
デフォルトで作成されていた雛形を削除し、以下の内容を記載します。YOUR_MAPBOX_ACCESS_TOKEN
はご自身のMapboxのパブリックトークンを使用してください。
import Map from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css';
const TOKEN = 'YOUR_MAPBOX_ACCESS_TOKEN';
function App() {
return (
<Map
initialViewState={{
longitude: 139.76711,
latitude: 35.68074,
zoom: 15
}}
style={{position: "absolute", top: 0, bottom: 0, width: "100%"}}
mapStyle="mapbox://styles/mapbox/streets-v12"
mapboxAccessToken={TOKEN}
>
</Map>
);
}
export default App;
Map
コンポーネントはいくつかのprops
をとります。
-
initialViewState
: 地図の初期位置、ズーム等を設定します。 -
style
: 地図を表示するコンテナのCSSを設定します。ここでは全領域に描画する設定にしています。 -
mapStyle
: 地図のスタイルを指定します。 -
mapboxAccessToken
: Mapboxのパブリックトークンを指定します。
App.css / index.css
今は特にスタイルを使用しないので中身をすべて消します。
実行
以下が実行結果です。SafariやFirefoxを使用されている方はデモが実行されない可能性があります。Chromeで表示するか、 https://stackblitz.com/edit/vitejs-vite-n3fywx を直接ご参照ください。
簡単に地図が表示できました。
Examples (Marker, Popup, NavigationControl and FullscreenControl) を試す
Examplesにいくつかのサンプルがあり、ウェブサイト上で挙動も試せます。ここではMarker, Popup, NavigationControl and FullscreenControlを参考に簡易化して記述します。
import {useState, useMemo} from 'react';
import Map, {Marker, Popup, NavigationControl} from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css';
const TOKEN = 'YOUR_MAPBOX_ACCESS_TOKEN';
const DATA = [
{"title":"東京駅","latitude":35.680949,"longitude":139.767144},
{"title":"東京タワー", "latitude":35.658621,"longitude":139.745435},
];
function App() {
const [popupInfo, setPopupInfo] = useState(null);
const pins = useMemo(() =>
DATA.map((data, index) => (
<Marker
key={`marker-${index}`}
longitude={data.longitude}
latitude={data.latitude}
anchor='bottom'
onClick={e => {
e.originalEvent.stopPropagation();
setPopupInfo(data);
}}
>
</Marker>
)), []);
return (
<Map
initialViewState={{
longitude: 139.76711,
latitude: 35.68074,
zoom: 12
}}
style={{position: "absolute", top: 0, bottom: 0, width: "100%"}}
mapStyle="mapbox://styles/mapbox/streets-v12"
mapboxAccessToken={TOKEN}
>
{pins}
{popupInfo && (
<Popup
longitude={Number(popupInfo.longitude)}
latitude={Number(popupInfo.latitude)}
anchor='top'
onClose={() => setPopupInfo(null)}
>
<div>
{popupInfo.title}
</div>
</Popup>
)}
<NavigationControl />
</Map>
);
}
export default App;
Markerを表示する
Data
の中にデータが入っており、それぞれのデータに対してMarkerコンポーネントを作成します。props
は基本的にMapbox GL JSのMarker
クラスのoptions
と一致しているのでわかりやすいかと思います。
Markerがクリックされた際にはsetPopupInfo
で該当するデータをステートに入れています。後ほど、Popupの表示で使用します。
また、useMemo
を使用することで、描画のたびにMarkerが生成されることを防いでいます。詳細はこちらのドキュメントをご参照ください。
const pins = useMemo(() =>
DATA.map((data, index) => (
<Marker
key={`marker-${index}`}
longitude={data.longitude}
latitude={data.latitude}
anchor='bottom'
onClick={e => {
e.originalEvent.stopPropagation();
setPopupInfo(data);
}}
>
</Marker>
)), []);
pins
はMarkerコンポーネントの配列ですが、以下のようにMapコンポーネントの子コンポーネントとして使用します。
<Map
...中略...
>
{pins}
...中略...
</Map>
MapコンポーネントはMapContext
の中にMapbox GL JSのMapオブジェクトを保持しています。そしてこのコンテキストを子コンポーネントに渡しています。
子コンポーネント側ではこのコンテキストを取り出して利用します。例えば、MarkerではここでMapを取得し、ここでMarkerオブジェクトをMapオブジェクトに追加します。
Popupを表示する
PopupコンポーネントもMapのコンテキストを使用するため、Mapコンポーネントの子コンポーネントとして配置します。
まず、popupInfo
の値を確認し、null
の場合は何も描画しません。MarkerがクリックされたときはpopupInfo
にdata
が入っているのでPopupコンポーネントを表示します。また、Popupの外部等がクリックされてonClose
が実行されるタイミングでpopupInfo
にnull
を設定して次回描画されないようにしています。
さらに、中身はPopupコンポーネントの子コンポーネントとして記述します。内部的には空のdivコンテナが作成されており、子コンポーネントがそのコンテナの子要素として描画される実装となっています。
また、Popupオブジェクトは直接Mapオブジェクトに追加されます。
{popupInfo && (
<Popup
longitude={Number(popupInfo.longitude)}
latitude={Number(popupInfo.latitude)}
anchor='top'
onClose={() => setPopupInfo(null)}
>
<div>
{popupInfo.title}
</div>
</Popup>
)}
Mapbox GL JSにおけるMarkerとPopupの一般的な使い方ではMarker#setPopup
を使うことでPopupの動的処理や表示位置を自動的に設定します。react-map-glではReactらしい記述ができますが、こういったオブジェクト同士をダイレクトに繋ぐ機能は少し犠牲になっているようです(Markerコンポーネントにprops.popup
を記述することもできますが、Reactらしさが犠牲になります)。
NavigationControlを表示する
NavigationControlコンポーネントも同様にMapのコンテキストを利用するので、Mapコンポーネントの子コンポーネントとして配置します。コントロールに関してはuseControl
というフックがあり、NavigationControlコンポーネントも内部的にはこれを利用しています。
<NavigationControl />
実行
以下が実行結果です。SafariやFirefoxを使用されている方はデモが実行されない可能性があります。Chromeで表示するか、 https://stackblitz.com/edit/vitejs-vite-qzdsmm を直接ご参照ください。
まとめ
react-map-glを用いることで、ReactっぽいコードでMapbox GL JSを利用できることがわかりました。
Discussion