先日公開した「Google Mapsエリアエディタ」は、特定のフレームワークに依存しないバニラJS(純粋なJavaScript)で実装しました。
バニラJSでドメインロジック(地図の計算や描画)を独立させておくと、ReactやVue、Svelteといったモダンなフロントエンド環境にもラップする(包む)だけで簡単に組み込むことができます。今回は、各フレームワークでの実装例をご紹介します。
※あらかじめお断り
私自身、別のプロジェクトでReactやSvelteを使用していますが、そちらの環境で直接Google Maps APIを組み込んで動作検証したわけではありません。
本記事に掲載しているコードは、AIに生成してもらった「実装の参考(未検証)」となります。実際のプロジェクトへ導入する際のヒントとしてご覧いただければ幸いです。
組み込みの基本コンセプト
どのフレームワークを使う場合でも、基本となる考え方は以下の3ステップで共通しています。
- DOMの確保: 地図を描画するための空の
div要素(コンテナ)への参照を取得する。 - マウント時(初期化): コンポーネントが画面に表示されたタイミングで、Google Maps APIと自作クラス(
AreaEditor)を初期化する。 - アンマウント時(破棄): 画面から消えるタイミングで、自作クラスの
destroy()メソッドを呼び出し、イベントリスナーやDOMを安全にクリーンアップする(メモリリーク防止)。
それでは、各フレームワークでのコード例を見てみましょう。
1. Reactでの使用例
Reactでは、DOMの参照に useRef を、初期化とクリーンアップに useEffect フックを使用します。
import { useEffect, useRef } from 'react';
import { AreaEditor } from './AreaEditor.js';
export default function AreaEditorMap() {
// 地図を描画するためのDOM参照
const mapRef = useRef(null);
useEffect(() => {
let editorInstance = null;
const initMap = async () => {
// APIのロードと地図の初期化
const { Map } = await google.maps.importLibrary("maps");
const map = new Map(mapRef.current, {
center: { lat: 35.681236, lng: 139.767125 },
zoom: 18,
mapId: 'DEMO_MAP_ID',
isFractionalZoomEnabled: false,
});
// エリアエディタの初期化
editorInstance = await AreaEditor.create(map, { types: [/* AreaTypeの定義 */] });
// ステート変更の購読
editorInstance.onStateChange = (state) => {
console.log("現在の状態:", state);
// ここでReactのuseStateを使ってUIを更新する等の処理が可能
};
};
initMap();
// クリーンアップ関数(アンマウント時に実行される)
return () => {
if (editorInstance) {
editorInstance.destroy();
}
};
}, []); // 空の依存配列で初回マウント時のみ実行
return <div ref={mapRef} style={{ width: '100%', height: '600px' }} />;
}
2. Vue 3 (Composition API) での使用例
Vue 3では、ref を使ってDOM要素を参照し、onMounted と onUnmounted ライフサイクルフックを使用します。
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { AreaEditor } from './AreaEditor.js';
const mapRef = ref(null);
let editorInstance = null;
onMounted(async () => {
const { Map } = await google.maps.importLibrary("maps");
const map = new Map(mapRef.value, {
center: { lat: 35.681236, lng: 139.767125 },
zoom: 18,
mapId: 'DEMO_MAP_ID',
isFractionalZoomEnabled: false,
});
editorInstance = await AreaEditor.create(map, { types: [/* AreaTypeの定義 */] });
editorInstance.onStateChange = (state) => {
console.log("現在の状態:", state);
};
});
// コンポーネント破棄時に安全にクリア
onUnmounted(() => {
if (editorInstance) {
editorInstance.destroy();
}
});
</script>
<template>
<div ref="mapRef" style="width: 100%; height: 600px;"></div>
</template>
3. Svelteでの使用例
Svelteは非常にシンプルに書けます。bind:this でDOM要素をバインドし、onMount フック内で初期化とクリーンアップ関数の返却を行います。
<script>
import { onMount } from 'svelte';
import { AreaEditor } from './AreaEditor.js';
let mapContainer;
let editorInstance;
onMount(() => {
// onMount自体は非同期にできないため、内部で即時実行関数を使用
(async () => {
const { Map } = await google.maps.importLibrary("maps");
const map = new Map(mapContainer, {
center: { lat: 35.681236, lng: 139.767125 },
zoom: 18,
mapId: 'DEMO_MAP_ID',
isFractionalZoomEnabled: false,
});
editorInstance = await AreaEditor.create(map, { types: [/* AreaTypeの定義 */] });
editorInstance.onStateChange = (state) => {
console.log("現在の状態:", state);
};
})();
// onMountが返す関数は、コンポーネント破棄時(onDestroy)に実行される
return () => {
if (editorInstance) {
editorInstance.destroy();
}
};
});
</script>
<div bind:this={mapContainer} style="width: 100%; height: 600px;"></div>
まとめ
UIライブラリの流行り廃りは早いですが、最も複雑になりがちな「地図上の座標計算」や「ベクトル演算」などのドメインロジックをバニラJSに閉じ込めておけば、どんなフレームワークを使っても使い回すことができます。
再利用性の高いコンポーネントを作る際は、「フレームワークの機能に依存しすぎない設計」を心がけるのも一つの手かもしれません。