Google Map

本日から3日間は外部APIや media element を利用した変わりダネを紹介します。今日は Google Map を埋め込み、現在地を表示するサンプルです。`$ yarn 1219` しただけでは挙動確認が出来ないので、以下手順でセットアップしてからお試し下さい。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/google-map) / `$ yarn 1219`

|![1219-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/4353a137-3430-7d9d-8c8b-cd399969ac38.jpeg)|![1219.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/71a10ebf-7681-49ca-1c35-e949c58c9745.jpeg)|
|---|---|

# setup

- Maps JavaScript API の APIキーを取得
- キーにアプリケーションの制限をかける（http://localhost:1234/など）
- キーにAPIの制限をかける（Maps JavaScript API)
- .env に GOOGLE_MAP_API_KEY=${YOUR_API_KEY} を記述


Google Map API を利用するにあたり、api_key が必要になります。環境変数から script tag に api_key を挿入しています。リポジトリルートに .env を作成し、取得した api_key を置きます。

```.env
GOOGLE_MAP_API_KEY=XXXXXX
```

サンプルには parcel を使用しており、react-async-script で 環境変数から script タグを生成（parcelの場合）

```app.tsx
import * as React from 'react'
import { render } from 'react-dom'
import Root from './components/index'
// @ts-ignore
import makeAsyncScriptLoader from 'react-async-script'
const URL = `https://maps.googleapis.com/maps/api/js?key=${
  process.env.GOOGLE_MAP_API_KEY
}`
const AsyncScriptRoot = makeAsyncScriptLoader(URL)(Root)
render(<AsyncScriptRoot />, document.getElementById('app'))
```

# 進行状況に応じた View 分岐

geolocation API を利用し、ユーザーの現在位置を利用します。現在位置取得出来るまで、3つの View に分岐します。

```components/index.tsx
export default () => (
  <GeolocationWrapper
    renderProcessView={() => <ProcessView />}
    renderSuccessView={coords => (
      <GoogleMapView coords={coords} />
    )}
    renderErrorView={() => <ErrorView />}
  />
)
```

`navigator.geolocation.getCurrentPosition` を call すると、現在位置取得の許可が求められます。現在位置もエラーも null なら進行中の View を、エラーがあればエラー View を、現在位置取得が出来たら GoogleMap API 利用の View をマウントします。

```components/geolocationWrapper.tsx
export default (props: Props) => {
  const [coords, setCoords] = useState<Coordinates | null>(
    null
  )
  const [error, setError] = useState<PositionError | null>(
    null
  )
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      ({ coords }) => {
        setCoords(coords)
      },
      setError
    )
  }, [])
  return (
    <>
      {useMemo(
        () => {
          if (coords === null) return
          return props.renderSuccessView(coords)
        },
        [coords]
      )}
      {useMemo(
        () => {
          if (error === null) return
          return props.renderErrorView(error)
        },
        [error]
      )}
      {useMemo(
        () => {
          if (!(error === null && coords === null)) return
          return props.renderProcessView()
        },
        [coords, error]
      )}
    </>
  )
}
```

# useEffect と GoogleMap API

GoogleMap API に則り、マウント時に取得した現在位置 coords をもって API を叩きます。地図をマウントするタグは ref で参照し、API callback 時に useState の更新関数で再描画させます。

```components/googleMapView.tsx
const View = (props: Props) => {
  const [addressLabel, setAddressLabel] = useState(
    '...取得しています'
  )
  const ref = useRef({} as HTMLDivElement)
  useEffect(
    () => {
      if (ref.current === null) return
      const { latitude, longitude } = props.coords // 取得した現在位置
      const center = { lat: latitude, lng: longitude }
      const mapOptions: google.maps.MapOptions = {
        center,
        zoom: 16
      }
      const map = new google.maps.Map(
        ref.current,
        mapOptions
      )
      const markerOptions: google.maps.MarkerOptions = {
        position: center,
        map
      }
      new google.maps.Marker(markerOptions)
      const geocoder = new google.maps.Geocoder()
      geocoder.geocode({ location: center }, function(
        results,
        status
      ) {
        if (status == google.maps.GeocoderStatus.OK) {
          setAddressLabel(results[1].formatted_address)
        } else {
          setAddressLabel('取得に失敗しました')
        }
      })
    },
    [props.coords]
  )
  return (
    <div className={props.className}>
      <div className="map" ref={ref} />
      <p className="addressLabel">
        現在住所：
        {addressLabel}
      </p>
    </div>
  )
}
```