たごもりすメモ

コードとかその他の話とか。

@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 />コンポーネントの呼び出しにonLoadonUnmountってフックがあったので、引数に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重に引かれてることになる。

どちらの呼び出しもsetStatepolylinenewしたオブジェクトを保存しているので、1回目の呼び出しでstateに保存されたPolylineオブジェクトは上書きされて消えてしまい、setMap(null);が呼ばれることがなくなってしまう、ということっぽい。

で、これはdevelopment mode的なやつでだけ起きてるんじゃないかなとnpm run buildしたものを手元で動かしてみた*3onLoadフックが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指定しても無効なの悲しいね。

*1:最初はGoogleのオフィシャルのReact Wrapper使おうと思ったんだけど、細かいところどうやるかのドキュメントが何もないのとロードがうまく動かないのと、あれこれあって諦めて使うライブラリをスイッチしたら一発でできた。なんだよ。

*2:JavaScriptのObjectにもobject_idがあったら便利なのに……。

*3:これをHTTPS有効にしてやるのにまたひとハマりした、ああもう