🗼

react-map-glを使ってみる

2024/07/18に公開

はじめに

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

今は特にスタイルを使用しないので中身をすべて消します。

実行

以下が実行結果です。SafariFirefoxを使用されている方はデモが実行されない可能性があります。Chromeで表示するか、 https://stackblitz.com/edit/vitejs-vite-n3fywx を直接ご参照ください。

簡単に地図が表示できました。

Examples (Marker, Popup, NavigationControl and FullscreenControl) を試す

Examplesにいくつかのサンプルがあり、ウェブサイト上で挙動も試せます。ここではMarker, Popup, NavigationControl and FullscreenControlを参考に簡易化して記述します。

App.jsx
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がクリックされたときはpopupInfodataが入っているのでPopupコンポーネントを表示します。また、Popupの外部等がクリックされてonCloseが実行されるタイミングでpopupInfonullを設定して次回描画されないようにしています。

さらに、中身は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コンポーネントも同様にMapのコンテキストを利用するので、Mapコンポーネントの子コンポーネントとして配置します。コントロールに関してはuseControlというフックがあり、NavigationControlコンポーネントも内部的にはこれを利用しています。

<NavigationControl />

実行

以下が実行結果です。SafariFirefoxを使用されている方はデモが実行されない可能性があります。Chromeで表示するか、 https://stackblitz.com/edit/vitejs-vite-qzdsmm を直接ご参照ください。

まとめ

react-map-glを用いることで、ReactっぽいコードでMapbox GL JSを利用できることがわかりました。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion