Usando mapbox-gl en React y Next.js

Introducción

En este artículo, quiero describir los métodos de incrustación que conozco mapbox-gl



en una React



aplicación, usando el ejemplo de creación de una aplicación web simple que contiene un mapa sobre el Next.js



uso Typescript



, el código del componente de mapa también se puede usar en cualquier aplicación enReact







Este artículo es parte de una serie de artículos.





Consideraré varias opciones de implementación usando el ejemplo de creación de un componente de mapa funcional:





  • Implementación con el almacenamiento de una instancia de mapa dentro de un React



    componente





  • Almacenar la instancia del mapa en el exterior React







Referencia de fragmentos de código

Para una cómoda lectura de este artículo, es necesario tener conocimientos básicos React



, Typescript



yCSS







Todos los fragmentos de código se usarán Typescript



, usar escribir en javascript es la mejor práctica, así que básicamente me atengo a él siempre que sea posible, me disculpo si no está familiarizado con él, aquí hay un gran curso de egghead.io donde puede usarlo para familiarizarse





Prefiero importar, React



ya import * as React from "react"



que puedes leer más sobre esto en el maravilloso artículo de Kent C. Dodds.





Si aparece en el código, // ...



debe leerse como lugares en los que falta el código duplicado.





Next.js



Typescript







npx create-next-app --typescript my-awesome-app
      
      



mapbox-gl



Typescript







cd my-awesome-app

npm install --save mapbox-gl && npm install -D @types/mapbox-gl
      
      



accessToken mapbox-gl







touch .env.local

echo NEXT_PUBLIC_MAPBOX_TOKEN=<_> >> .env.local
      
      



Next.js







.env.local





NEXT_PUBLIC_MAPBOX_TOKEN=<_>
      
      



React





rm styles/Home.module.css
      
      



styles/global.css





html,
body,
#__next {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}

* {
  box-sizing: border-box;
}

      
      



100%



, width



height



100%



html



body







css



#__next



Next.js



<div id="__next">...</div>







components/mapbox-map.tsx





import * as React from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css"; 
//   mapbox-gl    

function MapboxMap() {
    //       
  const [map, setMap] = React.useState<mapboxgl.Map>();

  // React ref     DOM    
  //     `container` 
  //    `mapbox-gl`
  // -   `null`
    const mapNode = React.useRef(null);

  React.useEffect(() => {
    const node = mapNode.current;
        //   window  ,
        //      
        //  dom node  ,    
    if (typeof window === "undefined" || node === null) return;

    //         DOM 
    //   accessToken  mapbox
    const mapboxMap = new mapboxgl.Map({
      container: node,
            accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
            style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
    });

    //       React.useState
    setMap(mapboxMap);
    
    //       
		//    
    return () => {
      mapboxMap.remove();
    };
  }, []);

    return <div ref={mapNode} style={{ width: "100%", height: "100%" }} />;
}

export default MapboxMap

      
      



mapbox-gl











pages/index.tsx





import MapboxMap from "../components/mapbox-map";

function App() {
  return <MapboxMap />;
}

export default App;

      
      



npm run dev
      
      



http://localhost:3000 -









  • - props







  • - -





  • - , , , .





https://app.mapflow.ai





, props



-





interface MapboxMapProps {
  initialOptions?: Omit<mapboxgl.MapboxOptions, "container">;
  onMapLoaded?(map: mapboxgl.Map): void;
  onMapRemoved?(): void;
}

function MapboxMap({ initialOptions = {}, onMapLoaded }: MapboxMapProps) {
    // ...

      
      



props







  • initialOptions - , container



    MapboxOptions



    , Omit







  • onMapLoaded -





  • onMapRemoved -





container



MapboxOptions



, Omit







initialOptions



-, spread syntax, , onMapLoaded



, props



, onMapRemoved







// ...
    const mapboxMap = new mapboxgl.Map({
      container: node,
      accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
      ...initialOptions,
    });

    setMap(mapboxMap);

    //  onMapLoaded ,    
    //    
    if (onMapLoaded) mapboxMap.once("load", onMapLoaded);

    return () => {
      mapboxMap.remove();
      if (onMapRemoved) onMapRemoved();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

// ...

      
      







// eslint-disable-next-line react-hooks/exhaustive-deps
      
      



react-hooks/exhaustive-deps



React.useEffect



[initialOptions, onMapLoaded]







, initialOptions



onMapLoaded



, React.useEffect











components/mapbox-map.tsx





import * as React from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";

interface MapboxMapProps {
  initialOptions?: Omit<mapboxgl.MapboxOptions, "container">;
  onMapLoaded?(map: mapboxgl.Map): void;
  onMapRemoved?(): void;
}

function MapboxMap({ 
  initialOptions = {}, onMapLoaded, onMapRemoved
}: MapboxMapProps) {
  const [map, setMap] = React.useState<mapboxgl.Map>();

  const mapNode = React.useRef(null);

  React.useEffect(() => {
    const node = mapNode.current;

    if (typeof window === "undefined" || node === null) return;

    const mapboxMap = new mapboxgl.Map({
      container: node,
      accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
      ...initialOptions,
    });

    setMap(mapboxMap);

    if (onMapLoaded) mapboxMap.once("load", onMapLoaded);
    
    return () => {
      mapboxMap.remove();
      if (onMapRemoved) onMapRemoved();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <div ref={mapNode} style={{ width: "100%", height: "100%" }} />;
}

export default MapboxMap;

      
      



onMapLoaded



. onMapLoaded



. onMapRemoved



.





, , .





MapLoadingHolder



.





svg



, Freepic, jsx https://svg2jsx.com/





components/world-icon.tsx





function WorldIcon({ className = "" }: { className?: string }) {
  return (
    <svg
      className={className}
      xmlns="http://www.w3.org/2000/svg"
      width="48.625"
      height="48.625"
      x="0"
      y="0"
      enableBackground="new 0 0 48.625 48.625"
      version="1.1"
      viewBox="0 0 48.625 48.625"
      xmlSpace="preserve"
    >
      <path d="M35.432 10.815L35.479 11.176 34.938 11.288 34.866 12.057 35.514 12.057 36.376 11.974 36.821 11.445 36.348 11.261 36.089 10.963 35.7 10.333 35.514 9.442 34.783 9.591 34.578 9.905 34.578 10.259 34.93 10.5z"></path>
      <path d="M34.809 11.111L34.848 10.629 34.419 10.444 33.819 10.583 33.374 11.297 33.374 11.76 33.893 11.76z"></path>
      <path d="M22.459 13.158l-.132.34h-.639v.33h.152l.022.162.392-.033.245-.152.064-.307.317-.027.125-.258-.291-.06-.255.005z"></path>
      <path d="M20.812 13.757L20.787 14.08 21.25 14.041 21.298 13.717 21.02 13.498z"></path>
      <path d="M48.619 24.061a24.552 24.552 0 00-.11-2.112 24.165 24.165 0 00-1.609-6.62c-.062-.155-.119-.312-.185-.465a24.341 24.341 0 00-4.939-7.441 24.19 24.19 0 00-1.11-1.086A24.22 24.22 0 0024.312 0c-6.345 0-12.126 2.445-16.46 6.44a24.6 24.6 0 00-2.78 3.035A24.18 24.18 0 000 24.312c0 13.407 10.907 24.313 24.313 24.313 9.43 0 17.617-5.4 21.647-13.268a24.081 24.081 0 002.285-6.795c.245-1.381.379-2.801.379-4.25.001-.084-.004-.167-.005-.251zm-4.576-9.717l.141-.158c.185.359.358.724.523 1.094l-.23-.009-.434.06v-.987zm-3.513-4.242l.004-1.086c.382.405.75.822 1.102 1.254l-.438.652-1.531-.014-.096-.319.959-.487zM11.202 7.403v-.041h.487l.042-.167h.797v.348l-.229.306h-1.098l.001-.446zm.778 1.085s.487-.083.529-.083 0 .486 0 .486l-1.098.069-.209-.25.778-.222zm33.612 9.651h-1.779l-1.084-.807-1.141.111v.696h-.361l-.39-.278-1.976-.501v-1.28l-2.504.195-.776.417h-.994l-.487-.049-1.207.67v1.261l-2.467 1.78.205.76h.5l-.131.724-.352.129-.019 1.892 2.132 2.428h.928l.056-.148h1.668l.481-.445h.946l.519.52 1.41.146-.187 1.875 1.565 2.763-.824 1.575.056.742.649.647v1.784l.852 1.146v1.482h.736c-4.096 5.029-10.33 8.25-17.305 8.25C12.009 46.625 2 36.615 2 24.312c0-3.097.636-6.049 1.781-8.732v-.696l.798-.969c.277-.523.574-1.033.891-1.53l.036.405-.926 1.125a22.14 22.14 0 00-.798 1.665v1.27l.927.446v1.765l.889 1.517.723.111.093-.52-.853-1.316-.167-1.279h.5l.211 1.316 1.233 1.799-.318.581.784 1.199 1.947.482v-.315l.779.111-.074.556.612.112.945.258 1.335 1.521 1.705.129.167 1.391-1.167.816-.055 1.242-.167.76 1.688 2.113.129.724s.612.166.687.166c.074 0 1.372.983 1.372.983v3.819l.463.13-.315 1.762.779 1.039-.144 1.746 1.029 1.809 1.321 1.154 1.328.024.13-.427-.976-.822.056-.408.175-.5.037-.51-.66-.02-.333-.418.548-.527.074-.398-.612-.175.036-.37.872-.132 1.326-.637.445-.816 1.391-1.78-.316-1.392.427-.741 1.279.039.861-.682.278-2.686.955-1.213.167-.779-.871-.279-.575-.943-1.965-.02-1.558-.594-.074-1.111-.52-.909-1.409-.021-.814-1.278-.723-.353-.037.39-1.316.078-.482-.671-1.373-.279-1.131 1.307-1.78-.302-.129-2.006-1.299-.222.521-.984-.149-.565-1.707 1.141-1.074-.131-.383-.839.234-.865.592-1.091 1.363-.69 2.632-.001-.007.803.946.44-.075-1.372.682-.686 1.376-.904.094-.636 1.372-1.428 1.459-.808-.129-.106.988-.93.362.096.166.208.375-.416.092-.041-.411-.058-.417-.139v-.4l.221-.181h.487l.223.098.193.39.236-.036v-.034l.068.023.684-.105.097-.334.39.098v.362l-.362.249h.001l.053.397 1.239.382.003.015.285-.024.019-.537-.982-.447-.056-.258.815-.278.036-.78-.852-.519-.056-1.315-1.168.574h-.426l.112-1.001-1.59-.375-.658.497v1.516l-1.183.375-.474.988-.514.083v-1.264l-1.112-.154-.556-.362-.224-.819 1.989-1.164.973-.296.098.654.542-.028.042-.329.567-.081.01-.115-.244-.101-.056-.348.697-.059.421-.438.023-.032.005.002.128-.132 1.465-.185.648.55-1.699.905 2.162.51.28-.723h.945l.334-.63-.668-.167v-.797l-2.095-.928-1.446.167-.816.427.056 1.038-.853-.13-.131-.574.817-.742-1.483-.074-.426.129-.185.5.556.094-.111.556-.945.056-.148.37-1.371.038s-.038-.778-.093-.778l1.075-.019.817-.798-.446-.223-.593.576-.984-.056-.593-.816h-1.261l-1.316.983h1.206l.11.353-.313.291 1.335.037.204.482-1.503-.056-.073-.371-.945-.204-.501-.278-1.125.009A22.188 22.188 0 0124.312 2c5.642 0 10.797 2.109 14.73 5.574l-.265.474-1.029.403-.434.471.1.549.531.074.32.8.916-.369.151 1.07h-.276l-.752-.111-.834.14-.807 1.14-1.154.181-.167.988.487.115-.141.635-1.146-.23-1.051.23-.223.585.182 1.228.617.289 1.035-.006.699-.063.213-.556 1.092-1.419.719.147.708-.64.132.5 1.742 1.175-.213.286-.785-.042.302.428.483.106.566-.236-.012-.682.251-.126-.202-.214-1.162-.648-.306-.861h.966l.309.306.832.717.035.867.862.918.321-1.258.597-.326.112 1.029.583.64 1.163-.02c.225.579.427 1.168.604 1.769l-.121.112zm-32.331-7.093l.584-.278.528.126-.182.709-.57.181-.36-.738zm3.099 1.669v.459h-1.334l-.5-.139.125-.32.641-.265h.876v.265h.192zm.614.64v.445l-.334.215-.416.077v-.737h.75zm-.376-.181v-.529l.459.418-.459.111zm.209 1.07v.433l-.319.32h-.709l.111-.486.335-.029.069-.167.513-.071zm-1.766-.889h.737l-.945 1.321-.39-.209.084-.556.514-.556zm3.018.737v.432h-.709l-.194-.28v-.402h.056l.847.25zm-.655-.594l.202-.212.341.212-.273.225-.27-.225zm28.55 5.767l.07-.082c.029.126.06.252.088.38l-.158-.298z"></path>
      <path d="M3.782 14.884v.696c.243-.568.511-1.122.798-1.665l-.798.969z"></path>
    </svg>
  );
}

export default WorldIcon;

      
      



components/map-loading-holder.tsx





import WorldIcon from "../components/world-icon";

function MapLoadingHolder() {
  return (
    <div className="loading-holder">
      <WorldIcon className="icon" />
      <h1>Initializing the map</h1>
      <div className="icon-attribute">
        Icons made by{" "}
        <a href="https://www.freepik.com" title="Freepik">
          Freepik
        </a>{" "}
        from{" "}
        <a href="https://www.flaticon.com/" title="Flaticon">
          www.flaticon.com
        </a>
      </div>
    </div>
  );
}

export default MapLoadingHolder;

      
      



, .app-container



, map-wrapper



MapLoadingHolder







<Head>...</Head>



- title







pages/index.tsx





import * as React from "react";
import Head from "next/head";
import MapboxMap from "../components/mapbox-map";
import MapLoadingHolder from "../components/map-loading-holder";

function App() {
  const [loading, setLoading] = React.useState(true);
  const handleMapLoading = () => setLoading(false);

  return (
    <>
      <Head>
        <title>Using mapbox-gl with React and Next.js</title>
      </Head>
      <div className="app-container">
        <div className="map-wrapper">
          <MapboxMap
            initialOptions={{ center: [38.0983, 55.7038] }}
            onMapLoaded={handleMapLoading}
          />
        </div>
        {loading && <MapLoadingHolder className="loading-holder" />}
      </div>
    </>
  );
}

export default App;

      
      



, .loading-holder



, , , , text-shadow: 0px 0px 10px rgba(152, 207, 195, 0.7);



<h1>Initializing the map</h1>



,





styles/global.css





html,
body,
#__next {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

* {
  box-sizing: border-box;
}

.app-container {
  width: 100%;
  height: 100%;
  position: relative;
}

.map-wrapper,
.loading-holder {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}

.loading-holder {
  background: -webkit-linear-gradient(
    45deg,
    rgba(152, 207, 195, 0.7),
    rgb(86, 181, 184)
  );
  background: -moz-linear-gradient(
    45deg,
    rgba(152, 207, 195, 0.7),
    rgb(86, 181, 184)
  );
  background: linear-gradient(
    45deg,
    rgba(152, 207, 195, 0.7),
    rgb(86, 181, 184),
    0.9
  );

  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.loading-holder .icon {
  transform: scale(2);
  fill: rgba(1, 1, 1, 0.7);
  animation: pulse 1.5s ease-in-out infinite;
}

.loading-holder h1 {
  margin-top: 4rem;
  text-shadow: 0px 0px 10px rgba(152, 207, 195, 0.7);
}

@keyframes pulse {
  0% {
    transform: scale(2);
  }
  50% {
    transform: scale(2.3);
  }
  100% {
    transform: scale(2);
  }
}

      
      











Almacenar la instancia del mapa fuera de React

Te contaré cómo almacenar y usar una instancia de un mapa en el mapbox-gl



exterior React



en mi próximo artículo.








All Articles