Как добавить карту и маркеры в приложение на React

Обзор

В этом руководстве рассказывается, как добавить карту и маркер в приложение на React с помощью @googlemaps/react-wrapper и интегрировать их в состояние приложения.

Как установить @googlemaps/react-wrapper

Установите библиотеку @googlemaps/react-wrapper и используйте ее для динамической загрузки Maps JavaScript API при отрисовке компонента.

npm install @googlemaps/react-wrapper

Эту библиотеку можно импортировать и использовать с помощью следующего кода:

import { Wrapper, Status } from "@googlemaps/react-wrapper";

Этот компонент в базовом виде используется в качестве оболочки для дочерних компонентов, которые зависят от Maps JavaScript API. Компонент Wrapper также принимает свойство render для отрисовки компонентов на этапе загрузки или обработки ошибок при загрузке Maps JavaScript API.

const render = (status: Status) => {
  return <h1>{status}</h1>;
};

<Wrapper apiKey={"YOUR_API_KEY"} render={render}>
  <YourComponent/>
</Wrapper>

Как добавить компонент карты

В базовом функциональном компоненте для отрисовки карты чаще всего используются хуки React useRef, useState и useEffect.

У исходного компонента карты будет следующая сигнатура:

const Map: React.FC<{}> = () => {};

Поскольку для google.maps.Map в качестве параметра конструктора требуется Element, хук useRef необходим для того, чтобы хранить изменяемый объект в течение срока жизни компонента. В приведенном ниже фрагменте кода создается экземпляр карты с хуком useEffect в компоненте Map.

TypeScript

const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

JavaScript

const ref = React.useRef(null);
const [map, setMap] = React.useState();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

Приведенный выше хук useEffect будет выполняться только при изменении ref. Теперь компонент Map будет возвращать следующий код:

return <div ref={ref} />

Как расширить компонент карты с помощью дополнительных свойств

Базовый компонент карты можно расширить с помощью свойств для параметров карты, прослушивателей событий и стилей, примененных к элементу div, содержащему карту. В коде ниже показан расширенный интерфейс функционального компонента.

interface MapProps extends google.maps.MapOptions {
  style: { [key: string]: string };
  onClick?: (e: google.maps.MapMouseEvent) => void;
  onIdle?: (map: google.maps.Map) => void;
}

const Map: React.FC<MapProps> = ({
  onClick,
  onIdle,
  children,
  style,
  ...options
}) => {}

Объект style можно передать напрямую и настроить как свойство отрисовываемого элемента div.

return <div ref={ref} style={style} />;

Для onClick, onIdle и google.maps.MapOptions необходимо, чтобы хуки useEffect принудительно применяли обновления к google.maps.Map.

TypeScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

JavaScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

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

TypeScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );

    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

JavaScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );
    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

Как создать компонент маркера

В компоненте маркера применяются шаблоны, похожие на используемые в компоненте карты с хуками useEffect и useState.

TypeScript

const Marker: React.FC<google.maps.MarkerOptions> = (options) => {
  const [marker, setMarker] = React.useState<google.maps.Marker>();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);

  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);

  return null;
};

JavaScript

const Marker = (options) => {
  const [marker, setMarker] = React.useState();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);
  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);
  return null;
};

Компонент возвращает null, поскольку для обработки DOM используется google.maps.Map.

Как добавить маркеры в виде дочернего компонента карты

Чтобы добавить маркеры к карте, компонент Marker будет передан компоненту Map с помощью специального свойства children, как показано ниже.

<Wrapper apiKey={"YOUR_API_KEY"}>
  <Map center={center} zoom={zoom}>
    <Marker position={position} />
  </Map>
</Wrapper>

Чтобы передавать объект google.maps.Map всем дочерним элементам в виде дополнительного свойства, нужно немного изменить выходные данные компонента Map.

TypeScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

JavaScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

Как связать карту и состояние приложения

Используя шаблон выше для обратных вызовов onClick и onIdle, приложение можно расширить, чтобы полностью интегрировать действия пользователя, например нажатие на карту и панорамирование.

TypeScript

const [clicks, setClicks] = React.useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState<google.maps.LatLngLiteral>({
  lat: 0,
  lng: 0,
});

const onClick = (e: google.maps.MapMouseEvent) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng!]);
};

const onIdle = (m: google.maps.Map) => {
  console.log("onIdle");
  setZoom(m.getZoom()!);
  setCenter(m.getCenter()!.toJSON());
};

JavaScript

const [clicks, setClicks] = React.useState([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState({
  lat: 0,
  lng: 0,
});

const onClick = (e) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng]);
};

const onIdle = (m) => {
  console.log("onIdle");
  setZoom(m.getZoom());
  setCenter(m.getCenter().toJSON());
};

Эти хуки можно интегрировать в элементы формы, используя шаблон ниже. В данном случае показано использование ввода широты.

<label htmlFor="lat">Latitude</label>
<input
  type="number"
  id="lat"
  name="lat"
  value={center.lat}
  onChange={(event) =>
    setCenter({ ...center, lat: Number(event.target.value) })
  }
/>

Наконец, приложение может отслеживать нажатия и отрисовывать маркеры в каждом месте, где было выполнено нажатие.

{clicks.map((latLng, i) => (<Marker key={i} position={latLng} />))}

Изучение кода

Чтобы изучить полный пример кода, можно использовать указанные ниже площадки для изучения кода в интернете или клонировать репозиторий Git.

Примеры кода

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

Для запуска примера в локальной среде необходимы Git и Node.js. Чтобы установить Node.js и NPM, следуйте этим инструкциям. Следующие команды используются, чтобы клонировать пример приложения, установить зависимости для него, а затем запустить его.

  git clone -b sample-react-map https://github.com/googlemaps/js-samples.git
  cd js-samples
  npm i
  npm start

Чтобы попробовать другие примеры, можно перейти в любую ветвь, которая начинается с sample-SAMPLE_NAME.

  git checkout sample-SAMPLE_NAME
  npm i
  npm start