Skip to content

Instantly share code, notes, and snippets.

@terrierscript
Created February 10, 2019 23:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save terrierscript/f57ace64b776848d68be6b5bc736e2ed to your computer and use it in GitHub Desktop.
Save terrierscript/f57ace64b776848d68be6b5bc736e2ed to your computer and use it in GitHub Desktop.
react-hooks-google-map-4-B
import { useEffect, useState, useCallback, useReducer, useRef } from "react";
import GoogleMapsApiLoader from "google-maps-api-loader";
// @ts-ignore
import uuid from "uuid/v4";
// 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]
); // initialConfigは変わったとしても再マウントするとおかしなことになるので更新対象にしない // googleMapかmapContainerRefが変化したらeffectが発火する。
// あとで使えるようにmapを返すようにする
return map;
};
// 単体でマーカーを描画する
export const useDrawMapMarker = ({
position,
googleMap,
map,
onClickMarker
}) => {
const markerObjectsRef = useRef(null);
useEffect(
() => {
const { Marker } = googleMap.maps;
// すでに描画済みなmarkerだったら描画しない
if (markerObjectsRef.current) {
return;
}
const markerObj = new Marker({
position,
map,
title: "marker!"
});
// markerがクリックされた時のイベントを追加する
markerObj.addListener("click", e => {
onClickMarker(e);
});
markerObjectsRef.current = markerObj;
// コンポーネントが消えたらmarkerもmapから消すように仕掛ける。これはすっ
return () => {
if (markerObjectsRef.current === null) {
return;
}
markerObjectsRef.current.setMap(null);
};
},
[googleMap, map]
);
};
const markerReducer = (state, action) => {
switch (action.type) {
case "ADD":
const id = uuid();
return {
...state,
[id]: action.payload
};
case "REMOVE":
const { [action.payload]: removeItem, ...rest } = state;
return rest;
default:
return state;
}
};
const mapReducerInitializer = initialMarkers => {
return initialMarkers.reduce((state, marker) => {
return markerReducer(state, {
type: "ADD",
payload: marker
});
}, {});
};
// markerをstate管理する
export const useMarkerState = initialMarkers => {
const [markers, dispatch] = useReducer(
markerReducer,
initialMarkers,
mapReducerInitializer
);
// マーカーの追加・削除のaction関数
const addMarker = useCallback(
position => dispatch({ type: "ADD", payload: position }),
[dispatch]
);
const removeMarker = useCallback(
removeUuid => dispatch({ type: "REMOVE", payload: removeUuid }),
[dispatch]
);
// 外向けにはobjectではなくarrayとして返す
const getMarkers = useCallback(
() => Object.entries(markers).map(([id, position]) => ({ id, position })),
[markers]
);
return {
// markers // objectとしてのmarkerは隠蔽する
addMarker,
removeMarker,
getMarkers
};
};
// Mapがクリックされたらイベントを飛ばすhooks
export const useMapClickEvent = ({ onClickMap, googleMap, map }) => {
useEffect(
() => {
const listener = googleMap.maps.event.addListener(map, "click", e => {
onClickMap({
lat: e.latLng.lat(),
lng: e.latLng.lng()
});
});
// onClickMapが変更されたらつくったイベントをクリアする
//(じゃないとクリックするたびにイベントが大量閣下さえる)
return () => {
googleMap.maps.event.removeListener(listener);
};
},
[googleMap, map, onClickMap]
);
};
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import {
useGoogleMap,
useMap,
useDrawMapMarker,
useMarkerState,
useMapClickEvent
} from "./hooks";
import styled from "styled-components";
const API_KEY = undefined;
const initialConfig = {
zoom: 12,
center: { lat: 35.6432027, lng: 139.6729435 }
};
const initialMarkers = [
{ 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 }
];
const MapContainer = styled.div`
height: 100vh;
width: 100%;
`;
// marker一つ一つを担当するComponent
const Marker = ({ googleMap, map, position, onClickMarker }) => {
useDrawMapMarker({
googleMap,
map,
position,
onClickMarker
});
return null;
};
const useMapMarkerSetup = ({ googleMap, map }) => {
const { addMarker, removeMarker, getMarkers } = useMarkerState(
initialMarkers
);
const markers = getMarkers();
// クリックイベントを追加
useMapClickEvent({
onClickMap: ({ lat, lng }) => {
addMarker({ lat, lng });
},
map,
googleMap
});
return { markers, removeMarker };
};
const MapMarkers = ({ map, googleMap }) => {
const { markers, removeMarker } = useMapMarkerSetup({ map, googleMap });
return (
<>
{markers.map(({ id, position }) => (
<Marker
key={id} // hooksがkeyに紐づく。これがないと適切なマーカーが消えなくなる
position={position}
onClickMarker={() => {
removeMarker(id);
}}
map={map}
googleMap={googleMap}
/>
))}
</>
);
};
const WaitForMap = ({ googleMap, map, children }) => {
if (!googleMap || !map) {
return null;
}
return children;
};
export const MapApp = () => {
const googleMap = useGoogleMap(API_KEY);
const mapContainerRef = useRef(null);
const map = useMap({
googleMap,
mapContainerRef,
initialConfig
});
return (
<>
<MapContainer ref={mapContainerRef} />
<WaitForMap googleMap={googleMap} map={map}>
<MapMarkers googleMap={googleMap} map={map} />
</WaitForMap>
</>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<MapApp />, rootElement);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment