Adicionar um mapa e marcadores a um aplicativo React

Visão geral

Neste tutorial, mostramos como adicionar um mapa e um marcador a um aplicativo React usando @googlemaps/react-wrapper e como integrar esses recursos ao estado do aplicativo.

Instalar @googlemaps/react-wrapper

Instale e use a biblioteca @googlemaps/react-wrapper para carregar dinamicamente a API Maps JavaScript quando o componente for renderizado.

npm install @googlemaps/react-wrapper

Essa biblioteca pode ser importada e usada com o código a seguir.

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

O uso básico desse componente é unir os componentes filhos que dependem da API Maps JavaScript. O componente Wrapper também aceita uma propriedade render para renderizar componentes de carregamento ou processar erros no carregamento da API Maps JavaScript.

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

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

Adicionar um componente de mapa

Um componente funcional básico para renderizar um mapa provavelmente vai usar os hooks useRef, useState e useEffect do React.

O componente inicial do mapa terá a seguinte assinatura.

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

Como google.maps.Map requer um Element como parâmetro construtor, useRef é necessário para manter um objeto mutável que vai persistir durante o ciclo de vida do componente. O snippet a seguir instancia um mapa no hook useEffect no corpo do 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]);

O hook useEffect acima só será executado quando ref mudar. O componente Map agora retorna o seguinte.

return <div ref={ref} />

Estender componente do mapa com mais propriedades

O componente básico do mapa pode ser estendido com outras propriedades para opções de mapas, listeners de eventos e estilos aplicados à div que contém o mapa. O código a seguir mostra a interface expandida desse 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
}) => {}

O objeto style pode ser transmitido diretamente e definido como uma propriedade no div renderizado.

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

onClick, onIdle e google.maps.MapOptions exigem que os hooks useEffect apliquem atualizações de forma imperativa a 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]);

Os listeners de eventos exigem um código um pouco mais complexo para apagar os listeners quando um gerenciador transmitido como uma propriedade é atualizado.

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

Criar um componente de marcador

O componente de marcador usa padrões semelhantes ao do mapa com hooks useEffect e 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;
};

O componente retorna nulo, porque google.maps.Map gerencia a manipulação do DOM.

Adicionar marcadores como um componente filho do mapa

Para adicionar os marcadores a um mapa, o componente Marker vai ser transmitido para o componente Map usando a propriedade children especial, como no exemplo a seguir.

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

Uma pequena mudança na saída do componente Map precisa ser feita para transmitir o objeto google.maps.Map a todos os filhos como uma propriedade extra.

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

Mapa de link e estado do aplicativo

Usando o padrão acima para chamadas de retorno de onClick e onIdle, o aplicativo pode ser estendido para integrar totalmente as ações do usuário, como clicar ou movimentar o 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());
};

Esses hooks podem ser integrados aos elementos do formulário usando o seguinte padrão, como demonstrado com a entrada de latitude.

<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 fim, o aplicativo pode rastrear cliques e marcadores de renderização em cada local do clique.

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

Explorar código

Analise o exemplo de código completo nos playgrounds de códigos on-line abaixo ou clonando o repositório git.

Testar amostra

Clonar amostra

O git e o Node.js são necessários para executar a amostra localmente. Siga estas instruções para instalar o Node.js e o NPM. Os comandos a seguir clonam, instalam dependências e iniciam o aplicativo de amostra.

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

Para testar outras amostras, alterne para qualquer ramificação que comece com sample-SAMPLE_NAME.

  git checkout sample-SAMPLE_NAME
  npm i
  npm start