總覽
本教學課程說明如何使用 @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>
新增地圖元件
算繪地圖的基本功能元件可能會使用 useRef
、useState
和 useEffect
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} />;
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; };
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 }); } })} </> );
連結地圖和應用程式狀態
將上述模式用於 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