Mapbox GL JSをReactの関数コンポーネントで表示する改


はじめに

以前に投稿した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 [email protected] @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()はスタイル全てを再レンダリングする事はなく、変更箇所のみを改めて描画してくれるようなのでパフォーマンスへの影響はないです(たぶん)。