React アプリケーションに Map と Marker を追加する

概要

このチュートリアルでは、@googlemaps/react-wrapper を使って React アプリケーションに Map と Marker を追加して、地図とマーカーをアプリケーション状態に組み込む方法について説明します。

@googlemaps/react-wrapper をインストールする

@googlemaps/react-wrapper ライブラリをインストールし、コンポーネントのレンダリング時にそのライブラリを使って、Maps JavaScript API を動的に読み込みます。

npm install @googlemaps/react-wrapper

このライブラリは、次のようにインポートして、使用できます。

import { Wrapper, Status } from "@googlemaps/react-wrapper";

このコンポーネントの基本的な使用方法は、Maps JavaScript API に依存する子コンポーネントをラップすることです。Wrapper コンポーネントは、読み込みコンポーネントのレンダリング、または Maps JavaScript API の読み込みエラーの処理を行う render プロパティも受け入れます。

const render = (status: Status) => {
  return <h1>{status}</h1>;
};

<Wrapper apiKey={"YOUR_API_KEY"} render={render}>
  <YourComponent/>
</Wrapper>

地図コンポーネントを追加する

地図をレンダリングする基本的な機能コンポーネントでは、ほとんどの場合 useRefuseStateuseEffect の React のフックが使用されます。

最初の地図コンポーネントには次のような署名があります。

const Map: React.FC<{}> = () => {};

google.maps.Map にはコンストラクタ パラメータとして Element が必要なため、コンポーネントの存続期間中は持続する可変オブジェクトを維持するために useRef が必要です。次のスニペットは、Map コンポーネントの本文の useEffect フック内で地図をインスタンス化します。

TypeScript

const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

JavaScript

const ref = React.useRef(null);
const [map, setMap] = React.useState();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

上記の useEffect フックは、ref が変更された場合にのみ実行されるようになり、現在は Map コンポーネントが次を返ようになりました。

return <div ref={ref} />

props を追加して地図コンポーネントを拡張する

地図のオプション、イベント リスナー、地図を含む div に適用されるスタイルの props を追加すると、基本的な地図コンポーネントを拡張できます。次のコードは、この機能コンポーネントの展開されたインターフェースを示しています。

interface MapProps extends google.maps.MapOptions {
  style: { [key: string]: string };
  onClick?: (e: google.maps.MapMouseEvent) => void;
  onIdle?: (map: google.maps.Map) => void;
}

const Map: React.FC<MapProps> = ({
  onClick,
  onIdle,
  children,
  style,
  ...options
}) => {}

style オブジェクトはそのまま渡され、レンダリングされる div でプロパティとして設定されます。

return <div ref={ref} style={style} />;

onClickonIdlegoogle.maps.MapOptions では、google.maps.Map に更新を強制的に適用するのに useEffect フックが必要です。

TypeScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

JavaScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

イベント リスナーでは、プロパティとして渡されたハンドラが更新された場合、既存のリスナーを削除するために、やや複雑なコードが必要になります。

TypeScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );

    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

JavaScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );
    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

マーカー コンポーネントを作成する

マーカー コンポーネントは、useEffect フックと useState フックを含む地図コンポーネントと同様のパターンを使用します。

TypeScript

const Marker: React.FC<google.maps.MarkerOptions> = (options) => {
  const [marker, setMarker] = React.useState<google.maps.Marker>();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);

  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);

  return null;
};

JavaScript

const Marker = (options) => {
  const [marker, setMarker] = React.useState();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);
  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);
  return null;
};

google.maps.Map が DOM 操作を管理するため、コンポーネントは null を返します。

地図の子コンポーネントとしてマーカーを追加する

地図にマーカーを追加するために、次のように、特別な children プロパティを使用して Marker コンポーネントが Map コンポーネントに渡されます。

<Wrapper apiKey={"YOUR_API_KEY"}>
  <Map center={center} zoom={zoom}>
    <Marker position={position} />
  </Map>
</Wrapper>

すべての子に追加のプロパティとして google.maps.Map オブジェクトを渡すには、Map コンポーネントの出力に若干の変更を加える必要があります。

TypeScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

JavaScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

地図とアプリケーション状態をリンクする

onClick コールバックと onIdle コールバックに上記のパターンを使用すると、アプリケーションを拡張して、地図のクリックやパンなどのユーザー アクションを完全に統合できます。

TypeScript

const [clicks, setClicks] = React.useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState<google.maps.LatLngLiteral>({
  lat: 0,
  lng: 0,
});

const onClick = (e: google.maps.MapMouseEvent) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng!]);
};

const onIdle = (m: google.maps.Map) => {
  console.log("onIdle");
  setZoom(m.getZoom()!);
  setCenter(m.getCenter()!.toJSON());
};

JavaScript

const [clicks, setClicks] = React.useState([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState({
  lat: 0,
  lng: 0,
});

const onClick = (e) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng]);
};

const onIdle = (m) => {
  console.log("onIdle");
  setZoom(m.getZoom());
  setCenter(m.getCenter().toJSON());
};

これらのフックは、緯度入力で示したように次のパターンを使用してフォーム要素に統合できます。

<label htmlFor="lat">Latitude</label>
<input
  type="number"
  id="lat"
  name="lat"
  value={center.lat}
  onChange={(event) =>
    setCenter({ ...center, lat: Number(event.target.value) })
  }
/>

これで、アプリケーションはクリックをトラッキングし、各クリックの位置にマーカーをレンダリングできます。

{clicks.map((latLng, i) => (<Marker key={i} position={latLng} />))}

コードを確認する

完全なサンプルコードについては、以下のオンライン コード プレイグラウンドを確認するか Git リポジトリのクローンを作成してください。

サンプルを試す

サンプルのクローンを作成する

このサンプルをローカルで実行するには、Git と Node.js が必要です。こちらの手順に沿って Node.js と NPM をインストールします。次のコマンドで、クローンを作成し、依存関係をインストールしてサンプル アプリケーションを起動します。

  git clone -b sample-react-map https://github.com/googlemaps/js-samples.git
  cd js-samples
  npm i
  npm start

他のサンプルを試すには、sample-SAMPLE_NAME で始まるブランチに切り替えます。

  git checkout sample-SAMPLE_NAME
  npm i
  npm start