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
yaimport * 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
.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
-
props
- -
- , , , .
, props
-
interface MapboxMapProps {
initialOptions?: Omit<mapboxgl.MapboxOptions, "container">;
onMapLoaded?(map: mapboxgl.Map): void;
onMapRemoved?(): void;
}
function MapboxMap({ initialOptions = {}, onMapLoaded }: MapboxMapProps) {
// ...
props
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.