Интеграция с React

Работа с Google Maps JavaScript API в связке с React требует учёта особенностей жизненного цикла компонентов, асинхронной загрузки внешних скриптов и управления императивным API внутри декларативной модели UI.

Основная сложность интеграции заключается в том, что Google Maps API построен на императивном подходе: карта создаётся как объект, которому напрямую передаются DOM-узлы и конфигурации, тогда как React оперирует виртуальным DOM и пересоздаёт UI через рендер-функции. Это приводит к необходимости изоляции карты от повторных рендеров и строгого контроля инициализации.


Подключение через <script> в index.html допустимо, но в современных приложениях чаще используется динамическая загрузка. Это позволяет контролировать момент инициализации и избежать блокировки рендера.

Распространённый подход — использование загрузчика @googlemaps/js-api-loader:

npm install @googlemaps/js-api-loader

Далее создаётся модуль загрузки:

import { Loader } from "@googlemaps/js-api-loader";

export const loader = new Loader({
  apiKey: "YOUR_API_KEY",
  version: "weekly",
  libraries: ["places"]
});

Ключевой момент: загрузка API должна происходить один раз на уровне приложения или кэширующего слоя, иначе возможны дублирующиеся запросы и гонки инициализации.


Инициализация карты внутри компонента

Базовый паттерн строится вокруг useRef, чтобы сохранить ссылку на DOM-узел и экземпляр карты.

import { useEffect, useRef } from "react";
import { loader } from "./googleMapsLoader";

function Map() {
  const mapRef = useRef(null);
  const mapInstance = useRef(null);

  useEffect(() => {
    let isMounted = true;

    loader.load().then(() => {
      if (!isMounted || !mapRef.current) return;

      mapInstance.current = new google.maps.Map(mapRef.current, {
        center: { lat: 40.7128, lng: -74.0060 },
        zoom: 10
      });
    });

    return () => {
      isMounted = false;
    };
  }, []);

  return <div ref={mapRef} style={{ width: "100%", height: "400px" }} />;
}

Ключевые аспекты:

  • useRef используется вместо state, чтобы избежать лишних ререндеров
  • карта создаётся только один раз
  • проверка isMounted предотвращает утечки при размонтировании компонента

Управление жизненным циклом карты

Карты Google не уничтожаются автоматически при размонтировании DOM-узла. Это требует явной очистки.

Хотя API не предоставляет прямого destroy, можно:

  • удалять слушатели событий
  • обнулять ссылки
  • очищать контейнер
useEffect(() => {
  return () => {
    if (mapInstance.current) {
      google.maps.event.clearInstanceListeners(mapInstance.current);
      mapInstance.current = null;
    }
  };
}, []);

Особенно важно это при частом монтировании компонентов, например в SPA с маршрутизацией.


Работа с маркерами внутри React

Маркер — отдельный императивный объект, который также не должен пересоздаваться при каждом рендере.

const markerRef = useRef(null);

useEffect(() => {
  if (!mapInstance.current) return;

  markerRef.current = new google.maps.Marker({
    position: { lat: 40.7128, lng: -74.0060 },
    map: mapInstance.current,
    title: "Marker"
  });

  return () => {
    if (markerRef.current) {
      markerRef.current.setMap(null);
      markerRef.current = null;
    }
  };
}, []);

Обновление состояния карты без пересоздания

В React важно отделять данные от визуальных объектов. Например, изменение центра карты не должно приводить к пересозданию карты.

function updateCenter(map, lat, lng) {
  map.setCenter({ lat, lng });
}

Использование state:

const [center, setCenter] = useState({ lat: 0, lng: 0 });

useEffect(() => {
  if (mapInstance.current) {
    mapInstance.current.setCenter(center);
  }
}, [center]);

Таким образом React управляет только данными, а API — визуальным состоянием.


Интеграция с хуками и абстракциями

Для масштабируемых проектов выделяется кастомный хук.

import { useEffect, useRef } from "react";
import { loader } from "./googleMapsLoader";

export function useGoogleMap(options) {
  const ref = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    let mounted = true;

    loader.load().then(() => {
      if (!mounted || !ref.current) return;

      mapRef.current = new google.maps.Map(ref.current, options);
    });

    return () => {
      mounted = false;
    };
  }, []);

  return { ref, map: mapRef };
}

Использование:

function MapView() {
  const { ref, map } = useGoogleMap({
    center: { lat: 51.5074, lng: -0.1278 },
    zoom: 12
  });

  return <div ref={ref} style={{ height: 500 }} />;
}

Работа с событиями API

Google Maps использует собственную систему событий.

useEffect(() => {
  if (!mapInstance.current) return;

  const listener = google.maps.event.addListener(
    mapInstance.current,
    "click",
    (e) => {
      console.log(e.latLng.lat(), e.latLng.lng());
    }
  );

  return () => {
    google.maps.event.removeListener(listener);
  };
}, []);

Важно:

  • слушатели должны удаляться
  • нельзя полагаться на сборщик мусора JavaScript

Проблемы повторного рендера React

Основная ошибка — создание карты внутри render-функции или зависимостей эффекта:

// неправильный подход
const map = new google.maps.Map(...)

Это приводит к:

  • утечкам памяти
  • дублированию DOM-слоёв
  • потере состояния карты

Правильная модель:

  • карта живёт в useRef
  • React управляет только контейнером и данными

Интеграция с TypeScript

При использовании TypeScript важно учитывать глобальные типы google.

npm install --save-dev @types/google.maps

Пример:

const mapRef = useRef<google.maps.Map | null>(null);

Это позволяет строго контролировать API и предотвращает обращения к null.


Асинхронные данные и синхронизация с картой

Частый сценарий — загрузка точек с сервера.

useEffect(() => {
  if (!mapInstance.current) return;

  fetch("/api/points")
    .then(res => res.json())
    .then(points => {
      points.forEach(p => {
        new google.maps.Marker({
          position: p,
          map: mapInstance.current
        });
      });
    });
}, []);

Проблема здесь — повторное добавление маркеров при обновлении данных. Решение:

  • хранить массив маркеров в ref
  • очищать перед обновлением

Оптимизация производительности

При большом количестве объектов на карте:

  • использовать кластеризацию (MarkerClusterer)
  • избегать частых setState
  • минимизировать пересоздание объектов API
  • мемоизировать конфигурации через useMemo
const mapOptions = useMemo(() => ({
  center,
  zoom: 10
}), [center]);

Использование в Next.js и SSR

В средах с серверным рендерингом window недоступен, поэтому инициализация должна происходить только на клиенте.

useEffect(() => {
  if (typeof window === "undefined") return;
  loader.load().then(initMap);
}, []);

Также часто требуется динамический импорт:

import dynamic from "next/dynamic";

const Map = dynamic(() => import("./Map"), {
  ssr: false
});

Архитектурное разделение

В крупных приложениях логично разделять уровни:

  • слой загрузки API
  • слой инициализации карты
  • слой бизнес-логики (маршруты, маркеры)
  • слой React UI

Это предотвращает смешение императивного API и декларативной модели интерфейса.


Управление несколькими картами

При наличии нескольких экземпляров карт:

  • каждая карта должна иметь собственный ref
  • нельзя делить один DOM-контейнер
  • конфигурации должны быть изолированы
const maps = useRef(new Map());

Интеграция пользовательских оверлеев

Custom overlays создаются через OverlayView:

class CustomOverlay extends google.maps.OverlayView {
  onAdd() {}
  draw() {}
  onRemove() {}
}

В React важно создавать такие объекты вне рендера и контролировать их жизненный цикл через useEffect.


Общие ошибки интеграции

  • создание карты при каждом рендере
  • отсутствие очистки слушателей
  • смешивание состояния React и состояния карты
  • прямое обращение к window.google без проверки загрузки
  • отсутствие защиты от повторной инициализации

Работа с Google Maps API в React требует строгого разделения ответственности между декларативной моделью интерфейса и императивной моделью визуализации карты, где React управляет только структурой и данными, а API отвечает за отрисовку и взаимодействие с географическими объектами.