0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js+Leaflet】Error: Map container is already initializedで沼った話

Last updated at Posted at 2024-11-15

初めに

今回は、Next.jsとLeafletを使って地図表示を実装しました。開発中にいくつかのエラーに悩まされ、解決までに時間がかかったため、その記録を記事として残します。同じ課題に直面している方の参考になれば幸いです。
また、さらに良い解決策があればコメントで教えていただけると助かります。

目次

  1. 地図表示の実装
  2. エラー解決策

地図表示の実装

まず、Next.jsのsrcディレクトリ内にcomponentsディレクトリを作成し、Map.tsxファイルを用意します。LeafletをReactで扱いやすくするためにreact-leafletライブラリをインストールしましょう。

npm install react-leaflet leaflet

Map.tsxは以下のように実装します。

src/components/Map.tsx
"use client";
import { MapContainer, Polygon, TileLayer } from "react-leaflet";
import "leaflet/dist/leaflet.css";

type Coordinate = [number, number];

export const Map = () => {

  return (
    <>
      <MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false} className="w-full h-[100vh]">
        <TileLayer attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
      </MapContainer>
    </>
  );
};

続いて、このMapコンポーネントをページに表示させてみましょう。

app/Leaflet/page.tsx

"use client";
import { Map } from "@/components/Map";

export default function Page() {
  return (
    <>
      <div className="px-3">
        <div className="flex justify-center text-3xl font-semibold text-[rgba(0,164,150,1)] m-5">Leafletアプリ</div>
        <Map />
      </div>
    </>
  );
}

これでブラウザを確認すると、
地図は表示されますが、以下のようなエラーがコンソールに出力されます。
image.png

ここのエラーの原因は、Leafletがwindowオブジェクトに依存しているためです。Next.jsではデフォルトでサーバーサイドレンダリング(SSR)が有効ですが、SSRの実行環境にはブラウザ固有のwindowやdocumentオブジェクトが存在しません。そのため、Leafletを利用する場合にはクライアントサイドでのみ実行されるように設定する必要があります。

そこで、dynamicインポートを使ってSSRを無効化し、クライアント側でのみレンダリングするように変更します。

以下のようにdynamic関数を追加してみましょう。

src/app/Leaflet/page.tsx
"use client";

import dynamic from "next/dynamic";

export default function Page() {
  const Map = dynamic(() => import("../../components/Map").then((mod) => mod.Map), { ssr: false });
  return (
    <>
      <div className="px-3">
        <div className="flex justify-center text-3xl font-semibold text-[rgba(0,164,150,1)] m-5">Leafletアプリ</div>
        <Map />
      </div>
    </>
  );
}

これでSSRエラーは解消されますが、
今度はブラウザ上で「Map container is already initialized」というエラーが発生しました。
image.png

MapContainerは一度生成されたL.mapインスタンスに対して、同じDOM要素に再生成されないように制御されています。しかし、dynamicでssr: falseに設定すると、クライアントサイドで再レンダリングが発生する場合があり、再度L.mapが初期化されようとすることでエラーが発生しています。

エラー解決法

このエラーを解決するために以下のことを試してみました。

  1. useRef関数の使用
  2. そもそもの実装方法の変更

useRef関数の使用

useRefを使用すると、コンポーネントの再レンダリング時にも同じ参照を保持できるため、L.mapインスタンスが再生成されることを防げます。Map.tsxを以下のように修正します。
Map.tsxを以下のように修正してみました。

src/components/Map.tsx
"use client";
import { MapContainer, Polygon, TileLayer } from "react-leaflet";
import "leaflet/dist/leaflet.css";

type Coordinate = [number, number];

export const Map = () => {
  const mapRef = useRef<HTMLDivElement>(null);
  return (
    <>
      <MapContainer center={[51.505, -0.09]} zoom={13} ref={mapRef}
      scrollWheelZoom={false} className="w-full h-[100vh]">
        <TileLayer attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
      </MapContainer>
    </>
  );
};

しかし、上記のコードではMapContainerがrefをサポートしていないため、エラーが発生してしまいます。
そのため、次の方法に切り替えます。

Leafletの直接使用

react-leafletを使用せずにleafletを直接使う方法です。leafletを直接使用すると、useEffect内での初期化とクリーンアップを制御できるため、不要な再初期化を防げます。

なので、Mapコンポーネントを以下のように変更してみました。

src/components/Map.tsx
"use client";

import { useLeafletMap } from "@/hooks/useLeafletMap";
import "leaflet/dist/leaflet.css";

export const Map = () => {
  const mapRef = useLeafletMap([33.5902, 130.4207]);

  return (
    <>
      <div ref={mapRef} className="w-full h-[100vh]" />
    </>
  );
};

useLeafletMapカスタムフックを以下のように作成します。
src/hooksディレクトリにuseLeafletMap.tsファイルを作成し、実装してください。

src/hooks/useLeafletMap.ts
"use client";

import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { Coordinate } from "@/utils/type";

const useLeafletMap = (center: Coordinate) => {
  const mapRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (typeof window !== "undefined" && mapRef.current) {
      const map = L.map(mapRef.current).setView(center, 13);

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      }).addTo(map);

      return () => {
        map.remove();
      };
    }
  }, [center]);

  return mapRef;
};

export { useLeafletMap };

最後にLeaflet/page.tsxを以下のように変更します。

src/app/Leaflet/page.tsx
"use client";

import dynamic from "next/dynamic";

export default function Page() {
  const Map = dynamic(() => import("../../components/Map").then((mod) => mod.Map), { ssr: false });
  return (
    <>
      <div className="px-3">
        <div className="flex justify-center text-3xl font-semibold text-[rgba(0,164,150,1)] m-5">Leafletアプリ</div>
        <Map />
      </div>
    </>
  );
}

これでブラウザを確認してみます。
するとちゃんと表示できました!ログのエラーも消えていると思います。
image.png

エラーが解決した理由として、leafletを直接使うことで、地図の初期化と削除のタイミングを手動で制御できたからと思います。
react-leafletでは再レンダリング時に複数の地図インスタンスが生成されて「Map container is already initialized」というエラーが出ていましたが、leafletを使ってuseEffect内で初期化し、アンマウント時にmap.remove()でインスタンスを削除することで、この問題が解消されたと思います。

終わりに

以上、Leafletのエラーに悩まされた経験を記事にまとめました。
react-leafletを使用して同様のエラーを解決できる方法があれば、ぜひ教えていただけると助かります。
最後まで読んでいただき、ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?