日本語版の記事はこちら (Japanese version is here)
Recently, I published the Google Maps Area Editor, which was implemented in Vanilla JS (pure JavaScript) independent of any specific framework.
By isolating domain logic (map calculations and rendering) in Vanilla JS, you can easily integrate it into modern frontend environments like React, Vue, and Svelte just by wrapping it. In this article, I will introduce implementation examples for each framework.
※ Disclaimer
Although I use React and Svelte in other projects, I haven’t directly integrated the Google Maps API into those environments for operation verification.
The code shown in this article is a “reference implementation (unverified)” generated with the help of AI. I hope you can use it as a hint when introducing it into your actual projects.
Basic Integration Concept
No matter which framework you use, the basic concept consists of the following 3 common steps:
- Secure the DOM: Get a reference to an empty
divelement (container) to render the map. - On Mount (Initialization): Initialize the Google Maps API and the custom class (
AreaEditor) when the component is displayed on the screen. - On Unmount (Destruction): Call the
destroy()method of the custom class when it disappears from the screen to safely clean up event listeners and the DOM (preventing memory leaks).
Now, let’s look at code examples for each framework.
1. Example in React
In React, we use the useRef hook for DOM references, and the useEffect hook for initialization and cleanup.
import { useEffect, useRef } from 'react';
import { AreaEditor } from './AreaEditor.js';
export default function AreaEditorMap() {
// DOM reference for rendering the map
const mapRef = useRef(null);
useEffect(() => {
let editorInstance = null;
const initMap = async () => {
// Load API and initialize the map
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,
});
// Initialize the Area Editor
editorInstance = await AreaEditor.create(map, { types: [/* AreaType definitions */] });
// Subscribe to state changes
editorInstance.onStateChange = (state) => {
console.log("Current state:", state);
// You can process UI updates here using React's useState, etc.
};
};
initMap();
// Cleanup function (executed on unmount)
return () => {
if (editorInstance) {
editorInstance.destroy();
}
};
}, []); // Empty dependency array to run only on initial mount
return <div ref={mapRef} style={{ width: '100%', height: '600px' }} />;
}
2. Example in Vue 3 (Composition API)
In Vue 3, we refer to DOM elements using ref, and use the onMounted and onUnmounted lifecycle hooks.
<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 definitions */] });
editorInstance.onStateChange = (state) => {
console.log("Current state:", state);
};
});
// Safely clean up when the component is destroyed
onUnmounted(() => {
if (editorInstance) {
editorInstance.destroy();
}
});
</script>
<template>
<div ref="mapRef" style="width: 100%; height: 600px;"></div>
</template>
3. Example in Svelte
Svelte can be written very simply. We bind the DOM element with bind:this, and handle initialization and returning the cleanup function within the onMount hook.
<script>
import { onMount } from 'svelte';
import { AreaEditor } from './AreaEditor.js';
let mapContainer;
let editorInstance;
onMount(() => {
// Since onMount itself cannot be async, we use an IIFE inside
(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 definitions */] });
editorInstance.onStateChange = (state) => {
console.log("Current state:", state);
};
})();
// The function returned by onMount is executed on component destruction (onDestroy)
return () => {
if (editorInstance) {
editorInstance.destroy();
}
};
});
</script>
<div bind:this={mapContainer} style="width: 100%; height: 600px;"></div>
Conclusion
Trends in UI libraries change quickly, but if you confine domain logic that tends to be the most complex—like coordinate calculations on maps and vector operations—to Vanilla JS, you can reuse it regardless of the framework.
When creating highly reusable components, “designing without relying too much on framework features” might be a good approach.