Cómo agregar un mapa y marcadores a una aplicación de React

Descripción general

En este instructivo, te mostramos cómo agregar un mapa y un marcador a una aplicación de React con @googlemaps/react-wrapper y, luego, cómo integrar el mapa y los marcadores en el estado de la aplicación.

Instala @googlemaps/react-wrapper

Instala y utiliza la biblioteca @googlemaps/react-wrapper para cargar de forma dinámica la API de Maps JavaScript cuando se renderice el componente.

npm install @googlemaps/react-wrapper

Esta biblioteca se puede importar y usar con el siguiente código:

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

El uso básico de este componente consiste en unir los componentes secundarios que dependen de la API de Maps JavaScript. El componente Wrapper también acepta un prop render para renderizar componentes de carga o controlar los errores de carga de la API de Maps JavaScript.

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

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

Cómo agregar un componente de mapa

Es probable que un componente funcional básico use los hooks useRef, useState y useEffect de React.

El componente de mapa inicial tendrá la siguiente firma.

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

Dado que google.maps.Map requiere un parámetro Element como parámetro de constructor, se necesita useRef para mantener un objeto mutable que persistirá durante la vida útil del componente. En el siguiente fragmento, se crea una instancia de un mapa dentro del hook useEffect en el cuerpo del componente 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]);

El hook useEffect anterior solo se ejecutará cuando cambie el ref. El componente Map ahora muestra lo siguiente.

return <div ref={ref} />

Cómo extender el componente del mapa con props adicionales

El componente de mapa básico se puede extender con props adicionales para opciones de mapas, objetos de escucha de eventos y diseños aplicados al elemento div que contiene el mapa. El siguiente código muestra la interfaz expandida de este componente funcional.

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
}) => {}

El objeto style se puede pasar directamente y configurarse como prop en el objeto div renderizado.

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

onClick, onIdle y google.maps.MapOptions requieren hooks useEffect para aplicar actualizaciones a google.maps.Map de forma imperativa.

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

Los objetos de escucha de eventos requieren un código un poco más complejo para borrar los objetos de escucha existentes cuando se actualiza un controlador como prop.

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

Cómo crear un componente de marcador

El componente de marcador usa patrones similares a los del componente de mapa con hooks useEffect y 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;
};

El componente muestra un valor nulo, ya que google.maps.Map administra la manipulación del DOM.

Cómo agregar marcadores como componentes secundarios del mapa

Para agregar los marcadores a un mapa, el componente Marker se pasa al componente Map con el prop especial children, como se muestra a continuación.

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

Se debe realizar un pequeño cambio en el resultado del componente Map para pasar el objeto google.maps.Map a todos los elementos secundarios como prop adicional.

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

Cómo vincular el mapa y el estado de aplicación

Con el patrón anterior para las devoluciones de llamada onClick y onIdle, la aplicación se puede extender a fin de integrar completamente las acciones de los usuarios, como los clics o los desplazamientos laterales en el mapa.

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

Estos hooks se pueden integrar en los elementos del formulario mediante el siguiente patrón, tal como se demuestra con la entrada de latitud.

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

Por último, la aplicación puede hacer un seguimiento de los clics y renderizar los marcadores en la ubicación de cada clic.

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

Cómo explorar el código

El código de muestra completo se puede explorar a través de las siguientes zonas de prueba del código en línea o mediante la clonación del repositorio de Git.

Prueba la muestra

Clona la muestra

Se requiere Git y Node.js para ejecutar esta muestra de manera local. Sigue estas instrucciones para instalar Node.js y NPM. Con los siguientes comandos, se clonan y se instalan las dependencias y se inicia la aplicación de la muestra.

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

Para probar otras muestras, cambia a cualquier rama que comience con sample-SAMPLE_NAME.

  git checkout sample-SAMPLE_NAME
  npm i
  npm start