Mostrando mapa de MapBox con React. 🗺️
Tags: javascript, beginners, react, spanish
Published: 2022-07-01 13:39:35

El propósito de esta publicación es enseñar como utilizar la librería de MapBox GL JS para mostrar mapas interactivos en aplicaciones de React JS.

En este caso vamos a mostrar un mapa, y agregarle un evento, que se ejecute al momento de dar doble click se coloque un marcador en esa posición a la que se acaba de dar doble click.

> 🚨 **Nota**: Este post requiere que sepas las bases de **React con TypeScript (hooks básicos y peticiones con fetch)**.
>

Cualquier tipo de Feedback o mejora es bienvenido, gracias y espero disfrutes el articulo. 🤗

---

## Tabla de contenido.

> 📌 [Tecnologías a utilizar.](#1)   
> 📌 [Antes de empezar a codear ...](#2)   
> 📌 [Creando el proyecto.](#3)   
> 📌 [Primeros pasos.](#3)   
> 📌 [Creando el componente para mostrar el mapa.](#5)   
> 📌 [Mostrando el mapa en pantalla.](#6)   
>> 📍 [Manteniendo la referencia al contenedor del mapa.](#7)   
>>> 🔴  [¿Por qué necesitamos mantener la referencia?](#8)   
>>
>> 📍 [Inicializando MapBox.](#9)   
>
> 📌 [Agregar un marcador en la posición inicial.](#10)   
>> 📍 [Antes de que crezca nuestro componente.](#11)  
>> 📍 [Escuchando el evento 'load' en el mapa.](#12)
>>> 🔴 [Creando la función para agregar marcadores.](#13)
>
>> 📍 [Mostrando el marcador.](#14)
>
> 📌 [Agregar un  nuevo marcador en el mapa cuando se haga doble click.](#15)  
> 📌 [Conclusión.](#16)  
> 📌 [Código fuente.](#17)

&nbsp;

## 🧵 Tecnologías a utilizar. <a id="1"></a>

- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ [MapBox](https://docs.mapbox.com/mapbox-gl-js/guides)
- ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post) 

&nbsp;

## 🧵 Antes de empezar a codear ... <a id="2"></a>

Antes de empezar a trabajar con el código tenemos que hacer un par de cosas para poder utilizar el mapa de MapBox.  

1- Tienes que crear una cuenta en MapBox.  
2- En tu cuenta buscaras el **access token** que te crea MapBox por defecto o si lo prefieres, puedes crear un nuevo token de acceso.
3- Guardar ese token de acceso para usarlo después.

&nbsp;

## 🧵 Creando el proyecto. <a id="3"></a>

Al proyecto le colocaremos el nombre de: **`show-mapbox`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.  
Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

```bash
cd show-mapbox
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

&nbsp;

## 🧵 Primeros pasos. <a id="4"></a>
Necesitamos instalar MapBox en nuestra aplicación:
```
npm i mapbox-gl
```
Y como estamos usando TypeScript, necesitamos instalar los tipos de MapBox:
```
npm i -D @types/mapbox-gl
```

Dentro de la carpeta **`src/App.tsx`** borramos todo el contenido del archivo y colocamos un **h1** que diga *"Hello world"* por mientras.

```js
const App = () => {
  return (
    <div>
        <h1>Hello World</h1>
    </div>
  )
}
export default App
```

> 🚨 **Nota**: Es necesario colocar los estilos de MapBox para que cuando estemos usando el mapa, este se vea de la mejor manera.
>
Lo mejor sera importar los estilos en el archivo **src/main.tsx**, en lo mas alto de nuestra aplicación.
Linea a colocar:
```
import 'mapbox-gl/dist/mapbox-gl.css'
```
Asi se vería el archivo **src/main.tsx** 
```js
import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';

import 'mapbox-gl/dist/mapbox-gl.css';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
```

&nbsp;

## 🧵 Creando el componente para mostrar el mapa. <a id="5"></a>

Creamos la carpeta **src/components** y creamos el archivo **MapView.tsx**
Y lo único que necesitamos para mostrar el mapa es una etiqueta **div**

> 🚨 **Nota**: Antes de empezar a mostrar el mapa; a este componente se le debe dar estilos, un alto y un ancho para que luego podamos visualizar el mapa correctamente.
>

```js
export const MapView = () => {
    return (
        <div className='map' />
    )
}
```

&nbsp;

## 🧵 Mostrando el mapa en pantalla. <a id="6"></a>

Para mostrar el mapa necesitaremos usar 2 hooks
El primero sera el useRef. Necesitamos useRef para guardar la referencia del div donde se va a renderizar el mapa.

El otro hook es el useEffect. Usaremos este hook para inicializar el mapa.

&nbsp;

### 🟠 **Manteniendo la referencia al contenedor del mapa.** <a id="7"></a>

Usamos el hook useRef para esta tarea, de la siguiente manera:

```js
import { useRef } from 'react';

export const MapView = () => {

    const mapRef = useRef<HTMLDivElement>(null);

    return <div ref={mapRef} className='map' />
}
```

&nbsp;

#### 🔴 **¿Por qué necesitamos mantener la referencia?** <a id="8"></a>

Bueno, podríamos solo colocar solo un **ID** al **div** y ya con eso funcionaria. 😌

El problema sera cuando queramos usar mas de un mapa. 🤔

Si usamos más de un componente **MapView**, solo se renderizaría un solo mapa por que tienen el mismo ID; y para evitar eso, usamos el hook useRef, ya que cada vez que reutilizamos el componente MapView se creara una nueva referencia.

&nbsp;

### 🟠 **Inicializando MapBox.** <a id="9"></a>

Creamos la carpeta **src/utils** y creamos un nuevo archivo llamado **initMap.ts** y ahí construiremos la función para inicializar el mapa.

Esta función tiene que recibir: 
- **container**: elemento HTML, en este caso el **div**, donde se renderizará el mapa.

- **coords**:  coordenadas del lugar. Tienen que ser de tipo array de dos números, donde la primera posición es la **longitud** y la segunda posición es la **latitud**.

``` js
import { Map } from 'mapbox-gl';

export const initMap = (container: HTMLDivElement, coords: [number, number]) => {
    
}
```

Dentro de la función vamos a retornar una nueva instancia de Map.   
La retornamos ya que vamos a necesitar esa instancia para hacer mas eventos y acciones. En el caso de que solo ocupes mostrar el mapa y ya, no sera necesario retornar nada.

``` js
import { Map } from 'mapbox-gl';

export const initMap = (container: HTMLDivElement, coords: [number, number]) => {
    
    return new Map();
}
```
La clase **Map** requiere ciertos opciones.

- **container**: el elemento HTML donde se renderizará el mapa, su valor sera el container que nos llega por parámetro de la función.

- **style**: Tipo de estilo del mapa, en este caso usare el dark, [en la documentación de MapBox vienen más estilos](https://docs.mapbox.com/api/maps/styles/#mapbox-styles).

- **pitchWithRotate**: es el control de inclinación del mapa, que en este caso lo queremos quitar, por eso colocamos **false**.

- **center**: son las coordenadas donde se posicionará el mapa al inicializarse, su valor serán las **coords** que nos llega por parámetro de la función.

- **zoom**: el zoom inicial del mapa, los niveles van de 0 a 22.

- **accessToken**: el token que guardamos con anterioridad. Por lo que te recomiendo guardar este token en una variable de entorno y usar dicha variable en esta propiedad de **accesToken**.

- **doubleClickZoom**: acción que se dispara al hacer doble click por defecto es el aumentar el zoom, pero lo pondremos el false, ya que usaremos la acción del doble click para otra tarea.

Y asi quedaría nuestra función lista para usarla. 😌

``` js
import { Map } from 'mapbox-gl';

export const initMap = (container: HTMLDivElement, coords: [number, number]) => {
    
    return new Map({
        container,
        style: 'mapbox://styles/mapbox/dark-v10',
        pitchWithRotate: false,
        center: coords,
        zoom: 15,
        accessToken: import.meta.env.VITE_KEY as string,
        doubleClickZoom: false
    });
    
}
```

Ahora de vuela en nuestro componente **MapView** usaremos el useEffect para llamar la función que hemos creado. 

Dentro del useEffect haremos una condición, donde solo si existe el valor de useRef, inicializaremos nuestro mapa.

En la función initMap, mandamos el elemento HTML que se encuentra en la propiedad ***current*** de **mapRef**,
después mandamos las coordenadas ( **[longitud, latitud]** ).

```js
import { useRef } from 'react';;
import { useMap } from '../hook/useMap';

export const MapView = () => {

    const mapRef = useRef<HTMLDivElement>(null);
    
    useEffect(() => {
        if (mapRef.current) {
            initMap(
                mapRef.current,
                [-100.31019063199852, 25.66901932031443]
            )
        }
    }, []);

    return (
        <div ref={mapRef} className='map' />
    )
}
```
Ahora, veríamos el mapa en pantalla 🥳, como en esta imagen:

![map](https://res.cloudinary.com/dnxchppfm/image/upload/v1656518892/posts/show-map/01_nqaxbh.png)

Bueno, y ahora que?
Que tal si agregamos unos eventos para agregar marcadores.😉

&nbsp;

## 🧵 Agregar un marcador en la posición inicial. <a id="10"></a>

Antes de crear eventos con el mapa, tenemos que mantener la referencia a la instancia de Map, para eso usaremos nuevamente **useRef**.

Creamos una nueva referencia llamada **mapInitRef** que sera de tipo map o null.

La función initMap retorna la instancia del Map, por lo que a mapInitRef le asignaremos dicha instancia.

```js
const mapInitRef = useRef<Map | null>(null);

useEffect(() => {
    if (mapRef.current) {

        mapInitRef.current = initMap(
            mapRef.current,
            [-100.31019063199852, 25.66901932031443]
        );

    }
}, []);
```

&nbsp;

### 🟠 **Antes de que crezca nuestro componente...** <a id="11"></a>
En este punto, sera mejor refactorizar nuestro código, creando un custom hook para manejar la lógica del mapa y dejar limpio nuestro componente **MapView**.

Creamos la carpeta **src/hooks** y dentro creamos el archivo **useMap.ts** y movemos la lógica del MapView al archivo **useMap.ts**.

Este custom hook recibe como parámetro el contenedor donde se renderizará el mapa. 

Ahora, remplazamos la palabra **mapRef** por **container**.

```js
import { useEffect, useRef } from 'react';
import { Map } from 'mapbox-gl';
import { initMap } from '../utils/initMap';

export const useMap = (container: React.RefObject<HTMLDivElement>) => {

    const mapInitRef = useRef<Map | null>(null);

    useEffect(() => {
        if (container.current) {

            mapInitRef.current = initMap(
                container.current,
                [-100.31019063199852, 25.66901932031443]
            );

        }
    }, []);
}
```

Después hacemos la llamada del hook en nuestro componente **MapView**. 

Y asi nos quedara nuestro componente, mucho mas legible. 😉

```js
import { useRef } from 'react';;
import { useMap } from '../hook/useMap';

export const MapView = () => {

    const mapRef = useRef<HTMLDivElement>(null);
    useMap(mapRef)

    return <div ref={mapRef} className='map' />
}
```

&nbsp;

### 🟠 **Escuchando el evento 'load' en el mapa.** <a id="12"></a>

Bueno, hasta ahora ya tenemos la referencia a la instancia del mapa a disposición.

Ahora lo que queremos hacer es que al cargar el mapa, se muestre un marcador en pantalla.

Para esto, la instancia de Map tiene el método **'on'** que nos permite escuchar ciertos eventos que se disparan en el mapa. 

Asi que, primero creamos un useEffect.

```js
useEffect(() => {

}, [])
```
Luego, vamos hacer una evaluación donde si el mapInitRef.current existe (o sea que tiene el valor de la instancia), 
ejecutamos el siguiente evento **'on()'**.

```js
useEffect(() => {
    
    mapInitRef.current && mapInitRef.current.on();

}, [])
```

El método on en este caso recibe 2 parámetros:
- **type**: es la acción a escuchar, en este caso sera la acción load, ya que queremos que se ejecute algo cuando ya haya cargado el mapa.
- **listener**: la función a ejecutar cuando se escuche la acción. 

```js
useEffect(() => {
    
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => {}
    )

}, [])
```


#### 🔴 **Creando la función para agregar marcadores.** <a id="13"></a>

Ahora vamos a crear una función para agregar marcadores al mapa.

Dentro de la carpeta **src/utils** creamos el archivo **generateNewMarker.ts** y agregamos una nueva función.

Esta función recibe como parámetro:
- **lat**: latitud.
- **lng**: longitud.
- **map**: el mapa al cual agregar el marcador.

```js
import { Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

}
```

Para crear un marcador hacemos una nueva instancia de la clase Marker, la cual le mandamos ciertos parámetros que son opcionales:
- **color**: color del marcador.
- **scale**: tamaño del marcador. 

```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    new Marker({ color: '#63df29', scale: 1.5 })
}
```
Después, ejecutamos el método **setLngLat** para mandarle la longitud y latitud como arreglo para decirle al marcador donde debe colocarse.

```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    new Marker({ color: '#63df29', scale: 1.5 })
        .setLngLat([lng, lat])
}
```
Y finalmente llamamos al método **addTo** para agregarlo al mapa, le pasamos la instancia del mapa que recibimos por parámetro.
```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    new Marker({ color: '#63df29', scale: 1.5 })
        .setLngLat([lng, lat])
        .addTo(map)
}
```
Un extra, seria crearle un PopUp. Para ello hacemos una nueva instancia de la clase **Popup** (la guardamos en una constante), la cual le mandamos ciertos parámetros que son opcionales:
- **closeButton**: mostrar el botón de cerrar, lo colocamos el falso.

- **anchor**: la posición donde debe mostrase el PopUp en el marcador.

```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    const popUp = new Popup({ closeButton: false, anchor: 'left', })

    new Marker({ color: '#63df29', scale: 1.5 })
        .setLngLat([lng, lat])
        .addTo(map)
}
```

Y para colocar contenido personalizado al **PopUp**, llamaremos al método **setHTML** y le mandamos HTML como un string.

```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    const popUp = new Popup({ closeButton: false, anchor: 'left', })
        .setHTML(`<div class="popup">You click here: <br/>[${lng},  ${lat}]</div>`)

    new Marker({ color: '#63df29', scale: 1.5 })
        .setLngLat([lng, lat])
        .addTo(map)
}
```

Finalmente, a la instancia del **Marker**, antes del método addTo, colocamos el método **setPopup** y le mandamos la constante **popUp**.

```js
import { Popup, Marker, Map } from 'mapbox-gl';

export const generateNewMarker = ({ lat, lng, map }: { lng: number, lat: number, map: Map }) => {

    const popUp = new Popup({ closeButton: false, anchor: 'left', })
        .setHTML(`<div class="popup">You click here: <br/>[${lng},  ${lat}]</div>`)

    new Marker({ color: '#63df29', scale: 1.5 })
        .setLngLat([lng, lat])
        .setPopup(popUp)
        .addTo(map)
}
```
Es hora de llamar este método! 😉

&nbsp;

### 🟠 **Mostrando el marcador** <a id="14"></a>
En nuestro hook useMap, dentro del useEffect donde estábamos creando agregando el evento para escuchar el mapa cuando cargue por primera vez, hacemos la llamada del método **generateNewMarker**.

```js
useEffect(() => {
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => generateNewMarker()
}, [])
```
A este método le mandamos un objeto que contenga: 
- map: le **mapInitRef.current** ya que es la instancia del mapa.
- el segundo parámetro mandamos **mapInitRef.current!.getCenter()**.  
Esta función retorna un array de dos números que son la longitud y latitud (estos números son los que le pasamos al principio, al momento de inicializar el mapa), por lo cual los esparcimos con el operador spread.

```js
useEffect(() => {
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => generateNewMarker({ 
            map: mapInitRef.current!, 
            ...mapInitRef.current!.getCenter() 
        })
}, [])
```

Por ultimo, es buena practica que cuando estamos escuchando eventos dentro de un useEffect, al momento de que se desmonte el componente (que en este caso no va a pasar ya que solo tenemos una vista que es la del mapa), es necesario dejar de escuchar el evento y no ejecutar nada.

```js
useEffect(() => {
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => generateNewMarker({ 
            map: mapInitRef.current!, 
            ...mapInitRef.current!.getCenter() 
        })

    return () => { 
        mapInitRef.current?.off('load', generateNewMarker) 
    }
}, [])
```

&nbsp;

Asi se vería el marcador en nuestro mapa. 🥳 

![initial marker](https://res.cloudinary.com/dnxchppfm/image/upload/v1656518893/posts/show-map/02_rubihe.png)

&nbsp;

## 🧵 Agregar un  nuevo marcador en el mapa cuando se haga doble click. <a id="15"></a>
Esto sera muy sencillo, ya que tenemos casi todo hecho.
Solo es necesario, agregar un nuevo efecto en nuestro custom hook.

Y siguiendo las mismas practicas que cuando escuchamos el evento **'load'** anteriormente.

- Hacemos la validación de que mapInitRef contenga la instancia del mapa.

- Llamamos al método on para escuchar el evento de **'dblclick'**.

- Ahora, el listener que se ejecuta nos de acceso a la longitud y latitud (que vienen como un array de dos números), las cuales podemos desestructurar del listener.

- Ejecutamos la función **generateNewMarker**.

- A la función generateNewMarker le mandamos el map, que tendrá el valor de la instancia del mapa que se encuentra en **mapInitRef.current**. Después, le esparcimos el valor de **lngLat** que nos otorga el listener.

- Limpiamos el efecto con el return, dejando de escuchar el evento **'dblclick'**


```js
useEffect(() => {
    
    mapInitRef.current && mapInitRef.current.on(
        'dblclick', 
        ({ lngLat }) => generateNewMarker({ 
            map: mapInitRef.current!, 
            ...lngLat 
        }))

    return () => { 
        mapInitRef.current?.off('dblclick', generateNewMarker) 
    }

}, [])
```

&nbsp;

Asi se verían los marcadores en nuestro mapa. 🥳 

![markers](https://res.cloudinary.com/dnxchppfm/image/upload/v1656518892/posts/show-map/03_riluwc.png)

&nbsp;

## 🧵 Conclusión. <a id="16"></a>
Todo el proceso que acabo de mostrar, es una de las formas en que se puede realizar el mostrar un mapa con React JS. 🗺️

Espero haberte ayudado a entender como realizar este ejercicio,muchas gracias por llegar hasta aquí! 🤗  

Te invito a que comentes si es que conoces alguna otra forma distinta o mejor de como hacer mostrar un mapa con React JS. 🙌

Y si te gusto el contenido, no olvides apoyarme reaccionando a esta publicación o compartiendo esta publicación con alguien que le interese! ❤️

&nbsp;

### 🧵 **Código fuente.** <a id="17"></a>
{%embed https://github.com/Franklin361/show-map %}