@react-google-maps/apiでの描画地図にPolylineで線を描くと消えなくなる
という問題が起きてあれこれやってた。React難しいの巻。たぶんnpm start
で起動できるDevelopment modeでだけ起きる問題。
問題
@react-google-maps/apiでReactアプリ上にGoogle Mapsを表示*1し、そこに好き勝手にマーカーとか線を描きたい。以下のような感じ。
// MyMapComponent import { LoadScript, GoogleMap, Marker, Polyline } from "@react-google-maps/api"; // ...中略 // in function MyMapComponent return ( <LoadScript googleMapsApiKey={myApiKey}> <GoogleMaps id="myMap" mapContainerStyle={{height: "80%", width: "100%}} zoom={calculatedZoom} center={{lat: calculatedCenterLat, lng: calculatedCenterLng}} mapOptions={{disabledDefaultUI: true, zoomControl: true}} > <Marker key={"map-marker-start-" + start.uuid} position={{lat: start.lat, lng: start.lng}} /> <Marker key={"map-marker-end-" + end.uuid} position={{lat: end.lat, lng: end.lng}} /> <Polyline key={"map-line-" + start.uuid + end.uuid} path={pathFromStartToEnd} /> </GoogleMaps> </LoadScript> );
start
とかend
あるいはpathFromStartToEnd
なんかはprops
で親から受け取る。これでまあうまく動く、ように見える。
なんだけど、外部から与えてるprops
の中身を変えてマーカーや線を再描画するとマーカーや線が残ることがあって、なんでなんだこれってだいぶ苦労した。
調査
いろいろ調べてると、Google Maps JavaScript APIのドキュメントにこんなのを見掛けた。
Removing Polylines | Maps JavaScript API | Google Developers
なんかこれがうまく呼べてないんだろうなってことで@react-google-maps/api
の実装を調べてみると、このコードを読む限りではthis.state.polyline.setMap(null);
してるように見える。おっかしーな。
で、調査してみようと以下のように<Polyline />
コンポーネントの呼び出しにonLoad
とonUnmount
ってフックがあったので、引数にstate
に格納しているpolyline
オブジェクトを受け取れる。これを以下のように指定して動かしてみた。
const onLoadHook = (line) => { console.log({message:"onLoad", line}); }; const onUnmountHook = (line) => { console.log({message:"onUnmount", line}); }; return ( // ... 中略 <Polyline key={"map-line-" + start.uuid + end.uuid} path={pathFromStartToEnd} onLoad={onLoadHook} onUnmount={onUnmountHook} /> // ... 中略 );
そしたらonLoad
は2回呼ばれてるのにonUnmount
は一回しか呼ばれてないことがわかった。えー。Polylineコンポーネントが作り直されて1回ずつ呼ばれたのか、Polylineコンポーネントは1回だけ作られて2回mountされたのかはよくわかってないんだけど *2 。でもたぶん後者かなという気がする。以下の推論がうまくハマるから。
ひとつのコンポーネントが2回マウントされているとすると、Polyline
オブジェクトが作られたあとcomponentDidMount
が2回呼ばれてるってことで、それぞれ呼び出しの中でnew google.maps.Polyline({...})
が行われてるから、実際の地図上には同じ線が2重に引かれてることになる。
どちらの呼び出しもsetState
でpolyline
にnew
したオブジェクトを保存しているので、1回目の呼び出しでstateに保存されたPolylineオブジェクトは上書きされて消えてしまい、setMap(null);
が呼ばれることがなくなってしまう、ということっぽい。
で、これはdevelopment mode的なやつでだけ起きてるんじゃないかなとnpm run build
したものを手元で動かしてみた*3らonLoad
フックが1回しか呼ばれなかったので、Production buildすればこの症状は起きない。
が、まあちょっと手元の開発でこれ起きてるの無視するのはねえ……。
解決策
しょうがないので自分でpolyline.setMap(null);
を確実に呼ぶようにする。以下のようにonLoad
フック経由で対象オブジェクトを受け取り、再レンダリング前のクリーンナップ時に過去描画されたものに対してsetMap(null);
を呼ぶ。
import { useEffect } from `react`; const lines = []; const onLoadHook = (line) => { lines.push(line); }; useEffect(() => { return () => { lines.forEach((line) => { line.setMap(null); }); }; }); return ( // ... 中略 <Polyline key={"map-line-" + start.uuid + end.uuid} path={pathFromStartToEnd} onLoad={onLoadHook} onUnmount={onUnmountHook} /> // ... 中略 );
これでうまくいった。useEffect
の使いかたがやっとちゃんとわかった気がする。