Интеграция Google Maps JavaScript API в React требует адаптации
императивной модели API к декларативной парадигме компонентов. Основная
сложность заключается в том, что карта создаётся и управляется вне
React-дерева, через глобальный объект google.maps, тогда
как React ожидает управляемого состояния и предсказуемого жизненного
цикла.
Типовая архитектура строится вокруг одного корневого компонента карты и набора дочерних компонентов-обёрток для маркеров, оверлеев и контролов.
Ключевая задача — корректная инициализация экземпляра карты и привязка его к DOM-элементу:
import { useEffect, useRef } from "react";
export function Map({ center, zoom }) {
const mapRef = useRef(null);
const instanceRef = useRef(null);
useEffect(() => {
if (!mapRef.current) return;
instanceRef.current = new google.maps.Map(mapRef.current, {
center,
zoom,
});
return () => {
instanceRef.current = null;
};
}, []);
useEffect(() => {
if (instanceRef.current) {
instanceRef.current.setCenter(center);
}
}, [center]);
useEffect(() => {
if (instanceRef.current) {
instanceRef.current.setZoom(zoom);
}
}, [zoom]);
return <div ref={mapRef} style={{ width: "100%", height: "400px" }} />;
}
В этой модели:
useEffectReact-компоненты, работающие с картой, делятся на два типа:
Map)Каждый дочерний компонент должен регистрироваться в родительском экземпляре карты.
Для устранения проп-дриллинга используется React Context:
import { createContext, useContext } from "react";
const MapContext = createContext(null);
export function useMap() {
return useContext(MapContext);
}
Обёртка провайдера:
export function MapProvider({ map, children }) {
return (
<MapContext.Provider value={map}>
{children}
</MapContext.Provider>
);
}
Маркер является типичным примером побочного объекта, который существует вне React.
import { useEffect } from "react";
import { useMap } from "./MapContext";
export function Marker({ position }) {
const map = useMap();
useEffect(() => {
if (!map) return;
const marker = new google.maps.Marker({
position,
map,
});
return () => {
marker.setMap(null);
};
}, [map, position]);
return null;
}
Особенности:
google.maps.MarkerОсновная проблема — рассинхронизация между React state и состоянием карты.
useEffect(() => {
if (!instanceRef.current) return;
const listener = instanceRef.current.addListener("center_changed", () => {
const newCenter = instanceRef.current.getCenter().toJSON();
onCenterChange(newCenter);
});
return () => listener.remove();
}, []);
Такой подход создаёт мост:
Google Maps JavaScript API использует событийную модель, не совпадающую с React Synthetic Events.
useEffect(() => {
if (!map) return;
const clickListener = map.addListener("click", (e) => {
console.log(e.latLng.toJSON());
});
return () => clickListener.remove();
}, [map]);
Для масштабируемой архитектуры события обычно абстрагируются:
export function useMapEvent(eventName, handler) {
const map = useMap();
useEffect(() => {
if (!map) return;
const listener = map.addListener(eventName, handler);
return () => listener.remove();
}, [map, eventName, handler]);
}
Хуки позволяют скрыть императивную природу API.
import { useEffect, useRef } from "react";
export function useGoogleMap(options) {
const ref = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (!ref.current) return;
mapRef.current = new google.maps.Map(ref.current, options);
return () => {
mapRef.current = null;
};
}, []);
return { ref, map: mapRef };
}
API загружается через внешний скрипт, что требует отдельного слоя инициализации.
export function loadGoogleMaps(apiKey) {
return new Promise((resolve) => {
if (window.google?.maps) {
resolve(window.google.maps);
return;
}
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`;
script.async = true;
script.onl oad = () => resolve(window.google.maps);
document.head.appendChild(script);
});
}
Компонент инициализации:
useEffect(() => {
loadGoogleMaps(apiKey).then(() => {
setReady(true);
});
}, []);
На практике часто используется обёртка
@react-google-maps/api, которая инкапсулирует загрузку и
базовые сущности карты.
Модель компонентов:
LoadScriptGoogleMapMarkerInfoWindowОднако внутренняя логика остаётся аналогичной: синхронизация imperative API с React lifecycle.
При большом количестве объектов используется кластеризация:
useEffect(() => {
if (!map) return;
const clusterer = new markerClusterer.MarkerClusterer({
map,
markers,
});
return () => clusterer.clearMarkers();
}, [map, markers]);
Проблемный аспект:
export const Marker = React.memo(function Marker(props) {
...
});
const position = useMemo(
() => ({ lat, lng }),
[lat, lng]
);
Без стабилизации объектов карта будет пересоздавать сущности при каждом рендере.
Контролируемая модель увеличивает сложность, но позволяет интегрировать карту в глобальный state (Redux, Zustand).
useEffect(() => {
if (!map) return;
const infoWindow = new google.maps.InfoWindow({
content: "<div>Text</div>",
});
infoWindow.open(map, marker);
return () => infoWindow.close();
}, [map, marker]);
Особенность:
Подход с порталами позволяет рендерить UI поверх карты:
import { createPortal } from "react-dom";
export function Overlay({ map, position, children }) {
const div = document.createElement("div");
useEffect(() => {
const overlay = new google.maps.OverlayView();
overlay.on Add = () => {
const panes = overlay.getPanes();
panes.overlayLayer.appendChild(div);
};
overlay.draw = () => {
const projection = overlay.getProjection();
const point = projection.fromLatLngToDivPixel(position);
div.style.transform = `translate(${point.x}px, ${point.y}px)`;
};
overlay.setMap(map);
return () => overlay.setMap(null);
}, [map, position]);
return createPortal(children, div);
}
interface MapProps {
center: { lat: number; lng: number };
zoom: number;
onCenterChange?: (pos: any) => void;
}
Типизация критична для:
google.maps.*google.maps объектов в state ReactuseEffectТиповая организация:
/map
Map.tsx
MapProvider.tsx
hooks/
useGoogleMap.ts
useMapEvent.ts
components/
Marker.tsx
Polyline.tsx
InfoWindow.tsx
Такое разделение позволяет: