Работа с картами в React требует строгого управления жизненным циклом компонентов, поскольку объект карты живёт вне React-дерева и напрямую взаимодействует с DOM и внешним API. При использовании Google Maps JavaScript API основная сложность заключается в синхронизации состояния React и императивного API картографической библиотеки.
Ключевая идея архитектуры — изолировать создание карты, управление слоями и подписки на события в кастомные hooks, минимизируя прямые побочные эффекты в компонентах.
Перед созданием карты необходимо гарантировать загрузку внешнего скрипта Google Maps. В React это удобно реализуется через отдельный hook, отвечающий только за инъекцию script-тега и контроль готовности API.
import { useEffect, useState } from "react";
export function useGoogleMapsScript(apiKey) {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (window.google?.maps) {
setLoaded(true);
return;
}
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
script.async = true;
script.onl oad = () => setLoaded(true);
document.head.appendChild(script);
return () => {
script.remove();
};
}, [apiKey]);
return loaded;
}
Особенности реализации:
window.google.mapsuseEffect как единой точки побочного
эффектаОбъект карты Google Maps нельзя хранить в state, поскольку его
обновления не должны триггерить React-render. Для этого используется
useRef.
import { useEffect, useRef, useState } from "react";
export function useMap(containerRef, options, isLoaded) {
const mapRef = useRef(null);
const [mapInstance, setMapInstance] = useState(null);
useEffect(() => {
if (!isLoaded || !containerRef.current || mapRef.current) return;
mapRef.current = new window.google.maps.Map(containerRef.current, {
center: options.center,
zoom: options.zoom,
disableDefaultUI: options.disableDefaultUI,
});
setMapInstance(mapRef.current);
}, [isLoaded, containerRef, options]);
return mapInstance;
}
Ключевые принципы:
useRef хранит императивный объект картыuseState используется только для передачи ссылки
наружуМаркер — типичный побочный объект, который должен синхронизироваться с React-данными.
import { useEffect, useRef } from "react";
export function useMarkers(map, markersData) {
const markersRef = useRef([]);
useEffect(() => {
if (!map) return;
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current = [];
markersData.forEach(item => {
const marker = new window.google.maps.Marker({
position: item.position,
map,
title: item.title,
});
markersRef.current.push(marker);
});
return () => {
markersRef.current.forEach(marker => marker.setMap(null));
markersRef.current = [];
};
}, [map, markersData]);
}
Особенности:
setMap(null)useRefGoogle Maps использует собственную систему событий
(addListener), не интегрированную с React synthetic events.
Поэтому требуется ручное управление подписками.
import { useEffect } from "react";
export function useMapEvents(map, handlers) {
useEffect(() => {
if (!map) return;
const listeners = [];
if (handlers.onClick) {
listeners.push(
map.addListener("click", handlers.onClick)
);
}
if (handlers.onZoomChanged) {
listeners.push(
map.addListener("zoom_changed", handlers.onZoomChanged)
);
}
return () => {
listeners.forEach(listener =>
window.google.maps.event.removeListener(listener)
);
};
}, [map, handlers]);
}
Основные моменты:
import React, { useRef } from "react";
import { useGoogleMapsScript } from "./useGoogleMapsScript";
import { useMap } from "./useMap";
import { useMarkers } from "./useMarkers";
import { useMapEvents } from "./useMapEvents";
export function Map({ apiKey, markers }) {
const containerRef = useRef(null);
const isLoaded = useGoogleMapsScript(apiKey);
const map = useMap(
containerRef,
{
center: { lat: 40.7128, lng: -74.006 },
zoom: 10,
disableDefaultUI: false,
},
isLoaded
);
useMarkers(map, markers);
useMapEvents(map, {
onClick: (e) => {
console.log(e.latLng.toJSON());
},
});
return <div ref={containerRef} style={{ width: "100%", height: "500px" }} />;
}
Архитектурные свойства:
При усложнении приложения требуется доступ к объекту карты из разных компонентов. Для этого используется Context API.
import { createContext, useContext } from "react";
export const MapContext = createContext(null);
export function useMapContext() {
return useContext(MapContext);
}
export function MapProvider({ map, children }) {
return (
<MapContext.Provider value={map}>
{children}
</MapContext.Provider>
);
}
Преимущества:
Синхронизация React state с состоянием карты требует аккуратного контроля, чтобы избежать бесконечных циклов обновления.
import { useEffect } from "react";
export function useMapViewSync(map, center, zoom) {
useEffect(() => {
if (!map) return;
map.setCenter(center);
}, [map, center]);
useEffect(() => {
if (!map) return;
map.setZoom(zoom);
}, [map, zoom]);
}
Принципы:
InfoWindow — типичный пример объекта, который должен существовать в единственном экземпляре.
import { useRef } from "react";
export function useInfoWindow(map) {
const infoWindowRef = useRef(null);
if (!infoWindowRef.current && window.google?.maps) {
infoWindowRef.current = new window.google.maps.InfoWindow();
}
const open = (marker, content) => {
if (!infoWindowRef.current) return;
infoWindowRef.current.setContent(content);
infoWindowRef.current.open(map, marker);
};
const close = () => {
infoWindowRef.current?.close();
};
return { open, close };
}
Характеристики:
При работе с картами критично избегать лишних пересозданий объектов.
Основные техники:
useRef для хранения инстансовuseMemo для опций картыuseCallbackuseEffectПример стабилизации опций:
const mapOptions = useMemo(() => ({
center,
zoom,
disableDefaultUI: true,
}), [center.lat, center.lng, zoom]);
Загрузка карты должна происходить только при необходимости, особенно в SPA и Next.js приложениях.
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./Map"), {
ssr: false,
});
Это предотвращает:
Интеграция с Places API часто выделяется в отдельный hook, работающий поверх Google Maps API:
Для тепловых карт:
google.maps.visualization.HeatmapLayerТиповая структура:
useGoogleMapsScript — загрузка APIuseMap — создание картыuseMarkers — управление точкамиuseMapEvents — событияuseMapViewSync — синхронизация состоянияuseInfoWindow — всплывающие окнаusePlacesAutocomplete — поискТакая декомпозиция формирует слой абстракции над императивным API, делая карту частью React-архитектуры без потери контроля над производительностью и жизненным циклом объектов.