はじめに
以前に投稿したReactの関数コンポーネントでMapbox GL JSを表示するデモというのが、割とあっさりanyを使っていたりデモと言えるのか怪しい出来だったので、そのリバイスを兼ねて、よりマシなサンプル実装を示します。
条件
- React Function Component + hooks
- TypeScript
環境構築
①React + Typescriptプロジェクト構築
npx create-react-app react-mapbox-demo --template typescript
※テンプレートのままだとReactの型定義がうまく適用されないかも、その場合改めてnpm install
すれば大丈夫です。
②Mapbox GL JSと型定義をインストール
npm install mapbox-gl@v1.13.0 @types/mapbox-gl
③サーバーたてる
npm start
コンポーネントのサンプル
import React, { useEffect, useState, useRef } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
// Mapbox Style
const mapStyle: mapboxgl.Style = {
version: 8,
sources: {
OSM: {
type: 'raster',
tiles: ['http://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution:
'<a href="http://osm.org/copyright">© OpenStreetMap contributors</a>',
},
},
layers: [
{
id: 'OSM',
type: 'raster',
source: 'OSM',
minzoom: 0,
maxzoom: 18,
},
],
};
const Map: React.FC = () => {
// mapboxgl.Mapのインスタンスへの参照を保存するためのuseState
const [mapInstance, setMapInstance] = useState<mapboxgl.Map>();
// 地図表示するDiv要素を特定するためのuseRef
const mapContainer = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// mapContainer.currentはnullになり得るので型ガード(ていねい)
if (!mapContainer.current) return;
const map = new mapboxgl.Map({
container: mapContainer.current, // ていねいな型ガードのおかげで必ずHTMLDivElementとして扱える、current!でも可
style: mapStyle,
center: [142.0, 40.0],
zoom: 4,
});
// mapboxgl.Mapのインスタンスへの参照を保存
setMapInstance(map);
}, []);
return <div style={{ height: 800 }} ref={mapContainer} />;
};
export default Map;
無事に地図画面が表示されました。
TIPS:地図のレイヤー構成の操作について
※以下の構成は上記のコードやや異なりますが、言いたいことに変わりはないので適宜読み替えてください
実際の地図アプリケーション開発では、地図のレイヤー構成を動的に更新したいかもしれません。Mapbox GL JSでは、その際、addSource()して、addLayer()して、でもその時sourceが既に存在していたら…とか色々めんどくさいです。なのでそれらのAPIは一切用いず、Style自体を操作する方法がおすすめです。以下が実装例。
import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
const MapDiv = styled.div`
height: 800px;
`;
// Mapbox Style
const initMapStyle: mapboxgl.Style = {
version: 8,
sources: {
OSM: {
type: 'raster',
tiles: ['http://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution:
'<a href="http://osm.org/copyright">© OpenStreetMap contributors</a>',
},
},
layers: [
{
id: 'OSM',
type: 'raster',
source: 'OSM',
minzoom: 0,
maxzoom: 18,
},
],
};
const emptyMapStyle: mapboxgl.Style = {
version: 8,
sources: {},
layers: [],
};
const Map: React.FC = () => {
// mapboxgl.Mapのインスタンスへの参照を保存するためのuseState
const [mapInstance, setMapInstance] = useState<mapboxgl.Map>();
// 地図表示するDiv要素をHTML要素を特定するためのuseRef
const mapContainer = useRef<HTMLDivElement | null>(null);
// 地図スタイルをstate管理
const [mapStyle, setMapStyle] = useState<mapboxgl.Style>(initMapStyle);
const [flag, setFlag] = useState(false);
// mapStyleの変更時に走る処理
useEffect(() => {
if (!mapInstance) {
// nullチェック
return;
}
// MapインスタンスのsetStyle()を実行
mapInstance.setStyle(mapStyle);
}, [mapStyle]);
useEffect(() => {
// 初回時のみ走る処理
if (!mapInstance) {
if (!mapContainer.current) {
// mapContainer.currentはnullになり得るので型ガード
return;
}
const map = new mapboxgl.Map({
container: mapContainer.current, // 型ガードのおかげで必ずHTMLDivElementとして扱える
style: mapStyle,
center: [142.0, 40.0],
zoom: 4,
});
// mapboxgl.Mapのインスタンスへの参照を保存
setMapInstance(map);
}
}, [mapInstance]);
return (
<MapDiv
ref={mapContainer}
onClick={() => {
// 以下のようにReactのstateを操作するとMapインスタンス側でsetStyle()が走る
setMapStyle(flag ? emptyMapStyle : initMapStyle);
setFlag(!flag);
}}
/>
);
};
export default Map;
単純にOSMスタイルと空スタイルを行き来するサンプルですが、React側の変数の操作だけでMapインスタンスのsetStyle()を発火させる事が出来ています。データの流れも非常にシンプルになります(Reactのスタイル変数の変更から、常に一方向にMapインスタンスへ反映される)。もしあちこちでaddSource()やaddLayer()を繰り返すと、Styleの管理がとても複雑になります(行数も増えてしまい何も良い事がありません)。
これはライブラリ自体の出来だと思いますが、setStyle()はスタイル全てを再レンダリングする事はなく、変更箇所のみを改めて描画してくれるようなのでパフォーマンスへの影響はないです(たぶん)。