Mapas com React usando Leaflet
Neste post vamos desenvolver uma página web para demonstrar, na prática, a integração de Mapas em uma aplicação com React usando Leaflet.
Alguns pontos que vamos abordar:
- Geolocalização;
- Consumo de API;
- Input com Autocomplete usando React-Select;
- Integração com Mapas;
- Estilização do Mapa e Marcadores.
Leaflet é uma biblioteca JavaScript open-source para trabalhar com Mapas em aplicações web e mobile. Pode ser simplesmente integrada a um site usando apenas HTML, CSS e JavaScript.
Podemos também integrar a Leaflet ao React com a biblioteca React Leaflet, que tem suporte ao TypeScript sendo bastante simples de utilizar. Ambas serão utilizadas em nossa aplicação de demonstração.
Somando todas essas tecnologias e conceitos, no final deste post vamos ter desenvolvido o app Entregas. Vai ser assim:
Conforme podemos observar na animação acima, quando o usuário digita o endereço, sugestões de locais semelhantes começam a aparecer no autocomplete. Quando algum endereço é selecionado, o pin aparece centralizado no mapa, mostrando a localidade escolhida.
Quando o usuário envia o formulário, o pin 📍 é atualizado para um ícone de pacote 📦. Para acessar os dados da entrega basta clicar no ícone — esse é o fluxo completo :)
📝 Pré-requisitos
Sempre queremos entregar a melhor experiência para nossa audiência.
Para uma melhor experiência com a leitura, você precisa entender o básico de:
- Como fazer requisições à API;
- React & TypeScript
- CSS
- NodeJS, Yarn ou Npm configurados e Create React App (CRA)
🔰 Iniciando o Projeto - CRA
Para criar um projeto React com TypeScript, execute o comando com Yarn:
yarn create react-app blog-react-maps-leaflet --template typescript
Ou com Npx (npm)
npx create-react-app blog-react-maps-leaflet --template typescript
Agora é só acessar o projeto para começar a codar:
cd blog-react-maps-Leaflet && code .
🌍 Instalando e configurando as bibliotecas
Para trabalhar com mapas instalamos as libs:
yarn add leaflet react-leaflet
A lib react-select
vai auxiliar com autocomplete no input de endereço, e o uuid
vai gerar um ID:
yarn add react-select uuid
Instalando as tipagens das libs:
yarn add -D @types/react-leaflet @types/react-leaflet @types/react-select
📂 Configurando a variável de ambiente
Sempre que formos trabalhar com APIs de mapas vamos precisar de um Token de acesso com o provedor. Mais uma vez estaremos usando o Mapbox.
É bem simples de criar a conta e pegar o access_token
.
Crie um arquivo .env
e adicione uma variável:
REACT_APP_ACCESS_TOKEN_MAP_BOX=SEU_ACCESS_TOKEN_MAP_BOX_AQUI
No arquivo .gitignore
adicione o .env
para não ser enviado ao Github.
No projeto criado com create-react-app
é obrigatório a variável começar com REACT_APP_
⚠️ Essa variável vai ficar protegida apenas no código, porém fica visível nas requisições, um usuário avançado consegue acessá-la no console/network do navegador.
📍 Configurando a API de Geolocalização.
Para simplificar, não vou criar várias pastas para organizar o projeto, ao invés disso vou manter todos os arquivos dentro de src
.
Na pasta src
crie um arquivo apiMapBox.ts
const ACCESS_TOKEN_MAP_BOX = `access_token=${process.env.REACT_APP_ACCESS_TOKEN_MAP_BOX}`
export const fetchLocalMapBox = (local: string) =>
fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${local}.json?${ACCESS_TOKEN_MAP_BOX}`
)
.then(response => response.json())
.then(data => data);
O código acima usa a Fetch API do JavaScript para buscar o local que o usuário informar e retorna os dados.
Exemplo:
https://api.mapbox.com/geocoding/v5/mapbox.places/Brasil.json?access_token=MAP_BOX_ACCESS_TOKEN
Resultado:
Para acessar os endpoints do Mapbox, clique aqui.
💅 Estilizando a aplicação
No arquivo src/index.css
, substitua o conteúdo atual pelo código abaixo.
Basicamente reseta as margens, padding, outline, box-sizing e define a fonte Nunito e um tamanho padrão para os elementos HTML:
* {
margin: 0;
padding: 0;
outline: 0;
box-sizing: border-box;
}
body,
input,
button,
textarea {
font: 600 18px Nunito, sans-serif;
}
No arquivo index.html
adicione a fonte Nunito do projeto:
<link
href="<https://fonts.googleapis.com/css2?family=Nunito:wght@600;700;800&display=swap>"
rel="stylesheet"
/>
Arquivo index.html
completo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link
href="<https://fonts.googleapis.com/css2?family=Nunito:wght@600;700;800&display=swap>"
rel="stylesheet"
/>
<title>Entregas</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
No arquivo src/App.css
, substitua o conteúdo:
#page-map {
width: 100vw;
height: 100vh;
}
form.landing-page-form {
width: 500px;
background: #ffffff;
border: 1px solid #d3e2e5;
border-radius: 30px;
padding: 20px 40px;
position: absolute;
top: 30px;
left: 40px;
z-index: 1;
}
#page-map .leaflet-container {
z-index: 0;
}
form.landing-page-form fieldset {
border: 0;
}
form.landing-page-form fieldset legend {
width: 100%;
font-size: 32px;
line-height: 34px;
color: #5c8599;
font-weight: 700;
border-bottom: 1px solid #d3e2e5;
margin-bottom: 20px;
padding-bottom: 20px;
}
form.landing-page-form .input-block + .input-block {
margin-top: 24px;
}
form.landing-page-form .input-block label {
display: flex;
color: #8fa7b3;
margin-bottom: 8px;
line-height: 24px;
}
form.landing-page-form .input-block label span {
font-size: 14px;
color: #8fa7b3;
margin-left: 24px;
line-height: 24px;
}
form.landing-page-form .input-block input {
width: 100%;
background: #f5f8fa;
border: 1px solid #d3e2e5;
border-radius: 20px;
outline: none;
color: #5c8599;
}
form.landing-page-form .input-block input {
height: 44px;
padding: 0 16px;
}
form.landing-page-form button.confirm-button {
margin-top: 34px;
width: 100%;
height: 64px;
border: 0;
cursor: pointer;
background-color: #4254f5;
border-radius: 20px;
color: #ffffff;
font-weight: 800;
transition: background-color 0.2s;
}
form.landing-page-form button.confirm-button:hover {
background-color: #6c79f5;
}
/* Pop Up - Marker */
#page-map .map-popup .leaflet-popup-content-wrapper {
background: rgba(255, 255, 255, 0.8);
border-radius: 20px;
box-shadow: none;
}
#page-map .map-popup .leaflet-popup-content h3 {
color: #0089a5;
font-size: 20px;
font-weight: bold;
margin: 8px 12px;
}
#page-map .map-popup .leaflet-popup-content p {
color: #042f38;
font-size: 12px;
font-weight: bold;
margin: 8px 12px;
line-height: 15px;
}
#page-map .map-popup .leaflet-popup-tip-container {
display: none;
}
/* Styling react select */
.filter__control {
border-radius: 20px !important;
width: 100% !important;
background: #f5f8fa !important;
border: 1px solid #d3e2e5 !important;
border-radius: 20px !important;
outline: none !important;
color: #5c8599 !important;
}
.filter__option {
background: #f5f8fa !important;
color: #5c8599 !important;
}
.filter__option--is-focused {
background: #d3e2e5 !important;
color: #010101 !important;
}
- Detalhes importantes sobre a estilização:
Se procurar no código alguma referência por leaflet-popup-content-wrapper
você não vai encontrar. Utilizei o inspecionador de código do Navegador para saber quais classes o Leaflet usa para fazermos as customizações nelas.
A mesma coisa para a lib do react-select
, no arquivo App.tsx
que veremos a seguir. Adicionei classNamePrefix="filter"
, um prefixo que na DOM aparece, por isso tem o filter__control
, por exemplo. Você não encontra uma referência a essa classe no código, ela só aparece quando a tela é montada no navegador.
Adicionamos !important
para forçar a estilização e sobrepor os valores padrão que vem na biblioteca.
Último aspecto importante é que o formulário fica flutuando na tela enquanto o mapa fica por baixo. Para isso usamos o z-index e position: absolute para o formulário.
O restante da estilização é mais simples.
Por fim, para concluir a aplicação, vamos criar a lógica e adicionar os componentes necessários.
🗺️ Adicionando o Mapa e o Formulário
Adicione os arquivos SVG dentro da pasta src
— package.svg | pin.svg
No arquivo src/App.tsx
substitua o conteúdo por:
Comentários por linha de código:
001 - importa a estilização da biblioteca leaflet;
002 a 014 - importa os arquivos e bibliotecas necessárias;
016 - declara a posição inicial que o mapa será renderizado (poderia usar API do navegador para pegar a localização atual do usuário, dentro do hook useEffect);
018 a 030 - customiza os marcadores package.svg e pin.svg para renderizar no mapa;
032 a 039 - define o contrato da interface Delivery com seus atributos;
041 a 044 - define o tipo Position que será explicado mais a frente seu caso de uso;
046 - começamos a criar a função App que irá conter toda a lógica do formulário e da renderização do mapa;
047 a 058 - são definidas as variáveis de estado do componente App usando useState.
deliveries
: array que vai ter todos os dados preenchidos no formulário quando o mesmo for salvo;position
: objeto que representa uma coordenada geográfica, será utilizado para mostrar o marcador (pin.svg) quando o usuário digitar sua localização;name
,complement
,address
: armazenam os dados do formulário;location
: variável auxiliar para centralizar o mapa assim que usuário escolhe um endereço.
060 a 074 - define a função loadOptions
que será disparada toda vez que o usuário digita algum endereço no input. Contém a implementação da busca do local (endereço) usando a API, retorna em formato de um array as opções para o autocomplete, o array é enviado pela função de retorno callback(places)
que preencherá as options do select;
076 a 088 - define a função handleChangeSelect
executada quando o usuário escolhe uma localização — adicionamos um marcador com setPosition, centralizamos o mapa com setLocation e adicionamos o endereço no estado com setAddress;
090 a 111 - define a função handleSubmit
, disparada assim que usuário clica no botão confirmar submetendo o formulário.
- Primeiro ela previne o comportamento padrão do navegador de fazer refresh na tela;
- Verifica se usuário informou o endereço ou nome — se não, o fluxo é interrompido;
- Adiciona no array de
deliveries
o objeto Delivery com suas propriedades. O id é gerado pelo uuidv4 em formato de string; - Por fim limpa os campos name, address, complement e reseta o position para remover o marcador pin.svg da tela.
113 a 199 - renderiza o mapa e o formulário na tela ;
132 a 139 - adiciona o componente AsyncSelect
do React Select:
<AsyncSelect
placeholder="Digite seu endereço..."
classNamePrefix="filter"
cacheOptions
loadOptions={loadOptions}
onChange={handleChangeSelect}
value={address}
/>
Como o próprio nome sugere, ele irá se comportar de maneira assíncrona e, enquanto estiver buscando os dados da API para carregar o options do select, vai exibir um loading no componente.
159 a 197 - monta em tela o componente Map
- 160 - Centraliza na posição inicial passando location
- 165 - TileLayer é o desenho do mapa — deixei comentado a TileLayer padrão, que não é muito bonito;
- 169 - Exibe o marcador da posição que o usuário escolheu no input do endereço, ele sai da tela assim que o usuário clica em confirmar do formulário;
- 176 a 196 - a cada elemento do array exibe um marcador (package.svg) na posição definida no objeto delivery. Quando usuário clica no marcador aparece um PopUp com os dados sobre a entrega;
202 - exporta a função App
que será importada no arquivo index.tsx
Fim da aplicação.
Esse é o resultado que temos no final da implementação:
Se quiser conferir o código fonte do projeto, clique aqui.
👊 Conclusão
Construímos uma aplicação de pequeno porte, porém com um conteúdo rico em informações sobre integração de mapas no React em 2020, usando React Hooks.
A escolha do Leaflet é interessante, porque é bem fácil de utilizar, a integração com React é muito boa e o suporte ao TypeScript funciona bem;
A escolha do react-select
para autocomplete foi devido a sua UI bem bonita, fácil de customizar e a DX (experiência de desenvolvimento) ser agradável com uma documentação excelente.
A documentação do react-select
recomenda que seja utilizado CSS-in-JS como exemplo o styled-components
, mas, para fins didáticos e práticos, usei o CSS; React-select é bem performática, pesquisei outras libs para fazer o autocomplete mas algumas tinham dependência com Google Places do Google Maps, por exemplo. Como react-select é agnóstica, serviu bem para esse propósito.
Segue aqui alguns desafios para praticar um pouco mais:
🔥 Desafios:
- Organizar os arquivos em pastas, separando os componentes;
- Criar o botão reset do formulário para apagar os dados digitados e o marcador do mapa caso usuário já tenha escolhido um endereço;
- Adicionar horário de entrega;
- Criar um botão para abrir a rota no Google Maps a partir da localização da entrega;
- Criar um botão no PopUp dos detalhes da entrega para remover a entrega e outro para editar;
- Melhorar a UI do PopUp;
- Replicar esse mesmo projeto usando o NextJS — bastante coisa deverão ser alteradas.
🚀 Fez algum desafio? Show! Agora você está ficando mais expert! 😎
Posta o link do repositório aqui nos comentários.
E aí, o que achou desse post?
Espero que tenha curtido! 💜
O aprendizado é contínuo e sempre haverá um próximo nível! 🚀