という問題が起きてあれこれやってた。React難しいの巻。たぶんnpm start
で起動できるDevelopment modeでだけ起きる問題。
問題
@react-google-maps/apiでReactアプリ上にGoogle Mapsを表示*1し、そこに好き勝手にマーカーとか線を描きたい。以下のような感じ。
import { LoadScript, GoogleMap, Marker, Polyline } from "@react-google-maps/api";
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
の使いかたがやっとちゃんとわかった気がする。
余談
はてなブログのMarkdown書式、コードハイライトの形式にjsx
指定しても無効なの悲しいね。