DEV Community

Cover image for Mostrando mapa de MapBox con React. 🗺️
Franklin Martinez
Franklin Martinez

Posted on

Mostrando mapa de MapBox con React. 🗺️

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.

📌 Antes de empezar a codear ...

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Creando el componente para mostrar el mapa.

📌 Mostrando el mapa en pantalla.

📍 Manteniendo la referencia al contenedor del mapa.

🔴 ¿Por qué necesitamos mantener la referencia?

📍 Inicializando MapBox.

📌 Agregar un marcador en la posición inicial.

📍 Antes de que crezca nuestro componente.

📍 Escuchando el evento 'load' en el mapa.

🔴 Creando la función para agregar marcadores.

📍 Mostrando el marcador.

📌 Agregar un nuevo marcador en el mapa cuando se haga doble click.

📌 Conclusión.

📌 Código fuente.

 

🧵 Tecnologías a utilizar.

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ MapBox
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

 

🧵 Antes de empezar a codear ...

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.

 

🧵 Creando el proyecto.

Al proyecto le colocaremos el nombre de: show-mapbox (opcional, tu le puedes poner el nombre que gustes).

npm init vite@latest
Enter fullscreen mode Exit fullscreen mode

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.

cd show-mapbox
Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.

npm install
Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).

code .
Enter fullscreen mode Exit fullscreen mode

 

🧵 Primeros pasos.

Necesitamos instalar MapBox en nuestra aplicación:

npm i mapbox-gl
Enter fullscreen mode Exit fullscreen mode

Y como estamos usando TypeScript, necesitamos instalar los tipos de MapBox:

npm i -D @types/mapbox-gl
Enter fullscreen mode Exit fullscreen mode

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

const App = () => {
  return (
    <div>
        <h1>Hello World</h1>
    </div>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

🚨 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'
Enter fullscreen mode Exit fullscreen mode

Asi se vería el archivo src/main.tsx

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>
)
Enter fullscreen mode Exit fullscreen mode

 

🧵 Creando el componente para mostrar el mapa.

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.

export const MapView = () => {
    return (
        <div className='map' />
    )
}
Enter fullscreen mode Exit fullscreen mode

 

🧵 Mostrando el mapa en pantalla.

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.

 

🟠 Manteniendo la referencia al contenedor del mapa.

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

import { useRef } from 'react';

export const MapView = () => {

    const mapRef = useRef<HTMLDivElement>(null);

    return <div ref={mapRef} className='map' />
}
Enter fullscreen mode Exit fullscreen mode

 

🔴 ¿Por qué necesitamos mantener la referencia?

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.

 

🟠 Inicializando MapBox.

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.

import { Map } from 'mapbox-gl';

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

}
Enter fullscreen mode Exit fullscreen mode

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.

import { Map } from 'mapbox-gl';

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

    return new Map();
}
Enter fullscreen mode Exit fullscreen mode

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.

  • 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. 😌

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

}
Enter fullscreen mode Exit fullscreen mode

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

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' />
    )
}
Enter fullscreen mode Exit fullscreen mode

Ahora, veríamos el mapa en pantalla 🥳, como en esta imagen:

map

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

 

🧵 Agregar un marcador en la posición inicial.

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.

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

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

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

    }
}, []);
Enter fullscreen mode Exit fullscreen mode

 

🟠 Antes de que crezca nuestro componente...

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.

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

        }
    }, []);
}
Enter fullscreen mode Exit fullscreen mode

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

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

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' />
}
Enter fullscreen mode Exit fullscreen mode

 

🟠 Escuchando el evento 'load' en el mapa.

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.

useEffect(() => {

}, [])
Enter fullscreen mode Exit fullscreen mode

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()'.

useEffect(() => {

    mapInitRef.current && mapInitRef.current.on();

}, [])
Enter fullscreen mode Exit fullscreen mode

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.
useEffect(() => {

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

}, [])
Enter fullscreen mode Exit fullscreen mode

🔴 Creando la función para agregar marcadores.

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.
import { Map } from 'mapbox-gl';

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

}
Enter fullscreen mode Exit fullscreen mode

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.
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 })
}
Enter fullscreen mode Exit fullscreen mode

Después, ejecutamos el método setLngLat para mandarle la longitud y latitud como arreglo para decirle al marcador donde debe colocarse.

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])
}
Enter fullscreen mode Exit fullscreen mode

Y finalmente llamamos al método addTo para agregarlo al mapa, le pasamos la instancia del mapa que recibimos por parámetro.

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

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)
}
Enter fullscreen mode Exit fullscreen mode

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

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)
}
Enter fullscreen mode Exit fullscreen mode

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

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)
}
Enter fullscreen mode Exit fullscreen mode

Es hora de llamar este método! 😉

 

🟠 Mostrando el marcador

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.

useEffect(() => {
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => generateNewMarker()
}, [])
Enter fullscreen mode Exit fullscreen mode

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.
useEffect(() => {
    mapInitRef.current && mapInitRef.current.on(
        'load', 
        () => generateNewMarker({ 
            map: mapInitRef.current!, 
            ...mapInitRef.current!.getCenter() 
        })
}, [])
Enter fullscreen mode Exit fullscreen mode

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.

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

    return () => { 
        mapInitRef.current?.off('load', generateNewMarker) 
    }
}, [])
Enter fullscreen mode Exit fullscreen mode

 

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

initial marker

 

🧵 Agregar un nuevo marcador en el mapa cuando se haga doble click.

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'

useEffect(() => {

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

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

}, [])
Enter fullscreen mode Exit fullscreen mode

 

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

markers

 

🧵 Conclusión.

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! ❤️

 

🧵 Código fuente.

GitHub logo Franklin361 / show-map

Application to display a map from the MapBox library and execute events to add markers on the map. 🗺️

Show MapBox map with React. 🗺️

Application to display a map from the MapBox library and execute events to add markers on the map. 🗺️

 

Image or Gif

 

Features ⚙️

  1. View a full screen map.
  2. Place a marker at the initial position when loading the map.
  3. Add a new marker when double clicking on the map.

 

Technologies 🧪

  • React JS
  • TypeScript
  • Vite JS
  • MapBox

 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/show-map
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies of the project.
    npm install
Enter fullscreen mode Exit fullscreen mode
  1. Run the project.
    npm run dev
Enter fullscreen mode Exit fullscreen mode

Note: For running the tests, use the following command

    npm run test
Enter fullscreen mode Exit fullscreen mode

 

Links ⛓️

Demo of the application 🔥

Here's the link to the tutorial in case you'd like to take a look at it! eyes 👀

  • 🇲🇽 🔗

  • 🇺🇲 🔗

Discussion (3)

Collapse
flash010603 profile image
Usuario163

Excelente post en español!

Collapse
dennistobar profile image
Dennis Tobar

Hola, super hiper mega completo el post <3. ¿Podrías cambiar una de las etiquetas del post a Spanish?, Así la comunidad que habla español podría encontrar este post :)

Collapse
franklin030601 profile image
Franklin Martinez Author

Excelente sugerencia Dennis!
Muchas gracias! 👐