在 React 應用程式中加入地圖和標記

總覽

本教學課程說明如何使用 @googlemaps/react-wrapper,在 React 應用程式中加入地圖和標記,並將地圖和標記整合到應用程式狀態中。

安裝 @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>

新增地圖元件

算繪地圖的基本功能元件可能會使用 useRefuseStateuseEffect React 掛鉤

初始地圖元件的特徵如下。

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

google.maps.Map 需要 Element 做為建構函式參數,因此需要使用 useRef 來維持元件生命週期內持續存在的可變物件。以下程式碼片段會在 Map 元件內文中的 useEffect 掛鉤內,將地圖執行個體化。

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} />;

onClickonIdlegoogle.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]);

建立標記元件

標記元件使用的模式與內含 useEffectuseState 掛鉤的地圖元件類似。

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;
};

google.maps.Map 會處理 DOM 操作,因此這個元件會傳回空值。

將標記新增為地圖的子元件

如要在地圖中加入標記,請使用特殊的 children 屬性將 Marker 元件傳遞至 Map 元件,如下所示。

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

請務必小幅調整 Map 元件的輸出內容,並以額外屬性的形式將 google.maps.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 });
      }
    })}
  </>
);

連結地圖和應用程式狀態

將上述模式用於 onClickonIdle 回呼時,系統可以擴充應用程式來完全整合使用者動作,例如按下或平移地圖。

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