BETA

React Hooksだけでライブラリ使わずにgoogle mapを利用する(基礎編)

投稿日:2019-02-11
最終更新:2019-02-11
※この記事は外部サイト(https://qiita.com/terrierscript/items/4a50...)からのクロス投稿です

Vue.jsでライブラリ使わずにgoogle mapを利用する のをReact Hooksでやってみたらすぐ書けてびっくりしたのでまとめる

今回は基礎編なので、useState, useEffectを中心に使う。本当はBasic Effectのみでやろうと思ったが、useRefはどうしても必要だったので今回利用している

ライブラリを使わず、と言いつつgoogle-maps-api-loaderを利用しているのはご了承いただきたい

1. まずMapの表示だけする

全体のコードがこんな感じ。
これで表示までできる。
useGoogleMapuseMapEffectという2つのhooksをここまでで作っている

// hooks.ts  

import { useEffect, useState } from "react"  
import GoogleMapsApiLoader from "google-maps-api-loader"  
const API_KEY = "XXXXXXXXXXXX"  

// Google Mapのオブジェクトを呼び出すだけのhooks  
export const useGoogleMap = (apiKey) => {  
  const [googleMap, setGoogleMap] = useState(null)  
  useEffect(() => {  
    GoogleMapsApiLoader({ apiKey }).then((google) => {  
      setGoogleMap(google)  
    })  
  }, []) // useEffectの第二引数を[]にすることで、初回1回目だけ実行される  
  return googleMap  
}  

// 実際にMapをDOMにマウントする処理。  
export const useMap = ({ googleMap, mapContainerRef, initialConfig }) => {  
  const [map, setMap] = useState(null)  
  useEffect(() => {  
    // googleMapかmapContainerRefが初期化されてなければ何もしない  
    if (!googleMap || !mapContainerRef.current) {  
      return  
    }  
    const map = new googleMap.maps.Map(mapContainerRef.current, initialConfig)  
    setMap(map)  
  },   
  // googleMapかmapContainerRefが変化したらeffectが発火する。  
  // initialConfigは変わったとしても再マウントするとおかしなことになるので更新対象にしない  
  [googleMap, mapContainerRef])  

  // あとで使えるようにmapを返すようにする  
  return map  
}
import React from "react";  
import { useGoogleMap, useMap } from "./hooks";  
import { useRef } from "react";  

const API_KEY = undefined  

const initialConfig = {  
  zoom: 12,  
  center: { lat: 35.6432027, lng: 139.6729435 }  
}  
// hookを利用して表示するコンポーネント  
export const MapApp = () => {  
  const googleMap = useGoogleMap(API_KEY)  
  const mapContainerRef = useRef(null)  
  useMap({ googleMap, mapContainerRef, initialConfig })  
  return (  
    <div  
      style={{  
        // ホントはstyled-componentsとかで良いのだけど簡略化  
        height: "100vh",  
        width: "100%"  
      }}  
      ref={mapContainerRef}  
    />  
  )  
}  

ReactDOM.render(<MapApp />, document.querySelector("#root"))

DEMO

デモがこちら(APIキーを設定してないのでdevモード)
https://codesandbox.io/s/zx060qr7q4

注釈:NGなhooksの書き方

ここで注意するとすれば例えば下記のような書き方はしてはいけない。

const useMapEffect = ({ googleMap, mapContainerRef, mapConfig }) => {  
  //  こう書くとhooksは動かない  
  if (!googleMap || !mapContainerRef.current) {  
    return  
  }  
  useEffect(() => {  
    const { Map } = googleMap.maps  
    new Map(mapContainerRef.current, mapConfig)  
    // 第二引数のいずれかが変更されたら再度処理される  
  }, [googleMap, mapContainerRef, mapConfig])  
}  

const useMapEffect = ({ googleMap, mapContainerRef, mapConfig }) => {  
  //  これもNGなパターン  
  if (googleMap && mapContainerRef.current) {  
    useEffect(() => {  
      const { Map } = googleMap.maps  
      new Map(mapContainerRef.current, mapConfig)  
      // 第二引数のいずれかが変更されたら再度処理される  
    }, [googleMap, mapContainerRef, mapConfig])  
  }  
}

Hooksはトップレベルでしか呼び出せず、ifやループの内部で呼ぶこともNGとなる。詳しくは下記にルールがある。
https://reactjs.org/docs/hooks-rules.html

eslint-plugin-react-hooksを使えばこれらエラーを検出してくれる。

ついでにいうとhooksはuseをprefixにしましょうという決まりごとがある
たとえば下記のようにhooksを利用しているにもかかわらずuseから始まらないというのも避けるべきだ。これらが無いとlinterもうまく動いてくれない
(逆にhooksが関係ないuseから始まる関数がlinterに誤検知される可能性もありそうな気がする?)

//  hooksなのに`use`から始まってないのもNG。ESLintで正しく検出できなくなる  
const myAwesomeHook = (...) => {  
  useEffect()  
}  

// 下記のようにhooksを生成する関数はLinter的にはOK。でもこのへん使い出すと可読性がヤバくなりそう(主観)  
function createHook() {  
  return function useHookWithHook() {  
    useHook();  
  }  
}

どんなものがOKでどんなものがNGかはテストケースのファイルを見ると色々書いてある

2. マーカーを表示する

ここにgoogleMap.maps.Markerを利用してマーカーを表示する

// markerを追加するhooksを追加  
export const useMapMarker = ({ markers, googleMap, map }) => {  
  useEffect(() => {  
    // 初期化がまだなら何もしない  
    if (!googleMap || !map) {  
      return  
    }  
    const { Marker } = googleMap.maps  
    const mapMarkerObj = markers.map(  
      (position) =>  
        new Marker({  
          position,  
          map,  
          title: "marker!"  
        })  
    )  
  }, [googleMap, map])  
}

ちなみに上記hooksはmarkersが変更は検知しないようにしている。markerが重なって多重に描画されることを避けるためだ。この対処については後述する

呼び出し側はこんな感じに呼び出す

const markers = [  
  { lat: 35.6432027, lng: 139.6729435 },  
  { lat: 35.5279833, lng: 139.6989209 },  
  { lat: 35.6563623, lng: 139.7215211 },  
  { lat: 35.6167531, lng: 139.5469376 },  
  { lat: 35.6950961, lng: 139.5037899 }  
]  

export const MapApp = () => {  
  const googleMap = useGoogleMap(API_KEY)  
  const mapContainerRef = useRef(null)  
  const map = useMap({  
    googleMap,  
    mapContainerRef,  
    initialConfig  
  })  
  // markerを渡す  
  useMapMarker({ markers, googleMap, map })  
  return (  
    <div  
      style={{  
        height: "100vh",  
        width: "100%"  
      }}  
      ref={mapContainerRef}  
    />  
  )  
}

DEMO

https://codesandbox.io/s/wknx1j1vxk

応用編

下記に続きます
https://terrier.qrunch.io//entries/WN3pcF4rSmEZc0vO

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@terrierの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!