Mi guía para usar mapas de leaflet en React con buscador y marcador al hacer clic. (Spanish-English)
Advertencia: Mucho texto xd.
Decidí crear esta guía dado que es confuso añadir el buscador y el marcador en el mapa de leaflet en react. La versión de la librería de leaflet de react cambio y la forma en que lo muestra la documentación de las librerías del buscador y del marcador no funciona.
Durante la implementación al no funcionar, me vi obligado a buscar en la web, con las soluciones encontradas realize la implementación para completar el requerimiento. Ahora lo resumo en este ejemplo y enlisto los pasos para que quién esté en la misma situación pueda implementarlo más fácilmente.
Espero esta simple guía sea de ayuda a quién esté tratando de implementar lo mismo o algo parecido. Así mismo, si tienes alguna idea para mejorar la implementación, añadir una funcionalidad o incluso mejorar la guía, corregir gramática tanto en su versión en español e inglés, no dudes en realizar el Pull request.
Si consideras que el presente ejemplo te sirvió, y quieres y tienes la posibilidad de apoyarme, no dudes en comprarme un café: https://www.buymeacoffee.com/FelipeGH
Para que leaflet funcione en react debes realizar primeramente lo siguiente:
Obtenido de la respuesta del usuario jlahd en: PaulLeCam/react-leaflet#881
1.- Incluir react-leaflet en la transpilación de babel, procesando correctamente los operadores ??.
npm add -D react-app-rewired react-app-rewire-babel-loader
package json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
2.- Entra al ejemplo, copia y pega el archivo config-overrides.js a la raíz de tu proyecto.
3.- Instalar leaflet: https://react-leaflet.js.org/docs/start-installation/
npm install react react-dom leaflet
npm install react-leaflet
Debes importar los estilos css de leaflet y darle un alto y ancho al mapa, de otra manera no se mostrará.
import "leaflet/dist/leaflet.css";
<MapContainer style={{height: "38rem", width:"100%"}} >...
4.- Importa lo siguiente:
import {MapConsumer, MapContainer, TileLayer, useMap} from 'react-leaflet';
5.- Agregar mapa a tu componente como lo muestra la documentación: https://react-leaflet.js.org/docs/start-setup/ Una vez agregado ya tendremos el mapa funcional, aún sin cuadro de búsqueda y selector de ubicación.
Puedes cambiar las coordenadas para poner las de tu preferencia en el atributo Center del componente MapContainer.
1.- Para poder utilizar el buscador, vamos a utilizar las siguientes librerías:
https://libraries.io/npm/leaflet-geosearch
npm install [email protected]
https://www.npmjs.com/package/leaflet-control-geocoder
npm i leaflet-control-geocoder
2.- Hacer los imports de las librerías instaladas en el paso anterior y de L
import L from "leaflet";
import LCG from 'leaflet-control-geocoder';
import {GeoSearchControl, OpenStreetMapProvider} from "leaflet-geosearch";
3.- importar el css del buscador:
import "leaflet-geosearch/dist/geosearch.css";
4.- En los hijos del MapContainer solo dejar "TileLayer"
5.- Crear la siguiente función para cargar el buscador:
function LeafletGeoSearch() {
const map = useMap();
const provider = new OpenStreetMapProvider();
const searchControl = new GeoSearchControl({
provider,
showMarker: false,
searchLabel: "Buscar dirección",
style: "bar"
});
useEffect(() => {
map.addControl(searchControl);
return () => map.removeControl(searchControl);
});
return null;
}
6.- Agregar la función como hijo de MapContainer:
<LeafletGeoSearch/>
Con la función agregada ya tendremos el buscador completamente funcional, para más información consultar su respectiva documentación. Enlaces adjuntados en el paso 1 de esta sección.
En esta sección se va a mover el contenido de MapContainer a una función, utilizando el hook useMemo, La razón es que al querer guardar los datos en una variable de estado (useState) el componente se renderizará y la librería realizará una nueva petición en cada selección, tantas que leaflet nos bloqueará por tantas peticiones.
1.- Asegúrate de tener todos los imports del paso anterior.
2.- Agrega el archivo con el nombre "constants" del proyecto ejemplo e y realiza su importación, con el nombre icon.
import icon from "./constants";
3.- Agrega la siguiente función:
function OnEventClick(map, onMarked) {
const marker = useRef(null);
const onInit = () => {
LCG.L = L;
const geocoder = LCG.L.Control.Geocoder.nominatim();
map.on("click", function (e) {
const {lat, lng} = e.latlng;
if (marker.current !== null) {
map.removeLayer(marker.current);
}
marker.current = L.marker([lat, lng], {icon}).addTo(map);
geocoder.reverse(e.latlng, map.options.crs.scale(map.getZoom()), results => {
let r = results[0];
if (r) {
marker.current.bindPopup(r?.name).openPopup();
onMarked(r);
}
});
});
};
useEffect(onInit, [map, onMarked]);
return null;
}
4.- Crea la variable de estado y su función para manejar los cambios:
const [markData, setMarkData] = useState({});
const handleMark = (data) => {
setMarkData({...data});
};
5.- El contenido de MapContainer envuélvelo en un useMemo:
const MapContent = useMemo(() => {
return (
<MapContainer center={[18.960336897236065, -99.225899768445]} zoom={12} scrollWheelZoom={true}
style={{height: "80%", width: "100%"}}>
<TileLayer
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LeafletGeoSearch/>
<MapConsumer>
{
(map) => OnEventClick(map, handleMark)
}
</MapConsumer>
</MapContainer>
);
}, []);
6.- El retorno del componente al final debe lucir así:
return (
<div style={{height: "100vh"}}>
{MapContent}
<div style={{
margin: "0 1rem"
}}>
<h2>Dirección seleccionada: </h2>
<h3>{markData?.name}</h3>
</div>
</div>
);
7.- Listo, has agregado el mapa, un buscador, un marcador al dar clic y obtener los datos de este último.
1.- Poner marcadores al dar clic sobre el mapa: https://codesandbox.io/s/4r7tc?file=/src/App.js:179-210
2.- Poner buscador: https://codesandbox.io/s/search-box-implementation-in-react-leaflet-v310-forked-ouezc?file=/src/MapWrapper.jsx
3.- Propiedades para el buscador: https://smeijer.github.io/leaflet-geosearch/leaflet-control
Alert: Too much text xd.
I decided to create this guide because, is confusing add a search input and a marker onClick in react leaflet map. Leaflet react library has updated and with that the way to implement change too. The documentation from search input and marker not work properly.
While I was implementing, it wasn't work, so I searched on the web, and with solutions found I be able to complete the requirement. So, now I make this example and guide with the steps in order to someone that have the same situation can do it easily.
I really hope this simple guide help you. Also, if you have a better idea to improve this guide, add new functionality or even improve or correct the grammar in english or spanish versions, don't doubt in make a pull request.
If you consider this example was helpful, and you want and have the possibility to support me, don't doubt to buy me a coffee: https://www.buymeacoffee.com/FelipeGH
In order to leaflet works, you need to do the following:
From jlahd answer on GitHub: PaulLeCam/react-leaflet#881
1- includes react-leaflet in babel transpilation, correctly processing the ?? operators
npm add -D react-app-rewired react-app-rewire-babel-loader
package json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
2- Copy config-overrides.js file from example and paste to your project route.
3- Install leaflet: https://react-leaflet.js.org/docs/start-installation/
npm install react react-dom leaflet
npm install react-leaflet
You must import leaflet css styles, and set width and height to the map, otherwise map will not show.
import "leaflet/dist/leaflet.css";
<MapContainer style={{height: "38rem", width:"100%"}} >...
4- Import the following:
import {MapConsumer, MapContainer, TileLayer, useMap} from 'react-leaflet';
5- Add the map to your component as documentation shows: https://react-leaflet.js.org/docs/start-setup/
Having done that, the map is ready to use, without search input and location marker.
You can change the coordinates in order to show map center in them, to do that use Center props in MapContainer component
1- In order to use search input, we need to use the following libraries:
https://libraries.io/npm/leaflet-geosearch
npm install [email protected]
https://www.npmjs.com/package/leaflet-control-geocoder
npm i leaflet-control-geocoder
2- Import the libraries installed in past step and L
import L from "leaflet";
import LCG from 'leaflet-control-geocoder';
import {GeoSearchControl, OpenStreetMapProvider} from "leaflet-geosearch";
3- Import css from search:
import "leaflet-geosearch/dist/geosearch.css";
4- In MapContainer make sure that TileLayer is the only child in this step
5- Add the following function to load the search:
function LeafletGeoSearch() {
const map = useMap();
const provider = new OpenStreetMapProvider();
const searchControl = new GeoSearchControl({
provider,
showMarker: false,
searchLabel: "Buscar dirección",
style: "bar"
});
useEffect(() => {
map.addControl(searchControl);
return () => map.removeControl(searchControl);
});
return null;
}
6- Add the previous function as child of MapContainer:
<LeafletGeoSearch/>
With done this, the search now is ready to use, for more info check out the documentation, links added in step 1 from this section.
In this section, the content of MapContainer will move using the hook useMemo. The reason of this, at saving the data using useState component will render for each click, so marker function will request a lot of times and then leaflet will block us for this.
1- Make sure to have all imports from previous step.
2- Add constants.js file from example project to yours, then import it as icon
import icon from "./constants";
3- Add the following function:
function OnEventClick(map, onMarked) {
const marker = useRef(null);
const onInit = () => {
LCG.L = L;
const geocoder = LCG.L.Control.Geocoder.nominatim();
map.on("click", function (e) {
const {lat, lng} = e.latlng;
if (marker.current !== null) {
map.removeLayer(marker.current);
}
marker.current = L.marker([lat, lng], {icon}).addTo(map);
geocoder.reverse(e.latlng, map.options.crs.scale(map.getZoom()), results => {
let r = results[0];
if (r) {
marker.current.bindPopup(r?.name).openPopup();
onMarked(r);
}
});
});
};
useEffect(onInit, [map, onMarked]);
return null;
}
4- Create state and the function to handle changes.
const [markData, setMarkData] = useState({});
const handleMark = (data) => {
setMarkData({...data});
};
5- The content of MapContainer wrapped on useMemo hook.
const MapContent = useMemo(() => {
return (
<MapContainer center={[18.960336897236065, -99.225899768445]} zoom={12} scrollWheelZoom={true}
style={{height: "80%", width: "100%"}}>
<TileLayer
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LeafletGeoSearch/>
<MapConsumer>
{
(map) => OnEventClick(map, handleMark)
}
</MapConsumer>
</MapContainer>
);
}, []);
6- At the end, the component return should look like following:
return (
<div style={{height: "100vh"}}>
{MapContent}
<div style={{
margin: "0 1rem"
}}>
<h2>Dirección seleccionada: </h2>
<h3>{markData?.name}</h3>
</div>
</div>
);
7- Congratulations, you have completed this guide, you have added a map, a search input and a marker obtaining its data.
1- Add onClick on MapContainer: https://codesandbox.io/s/4r7tc?file=/src/App.js:179-210
2- Search input implementation: https://codesandbox.io/s/search-box-implementation-in-react-leaflet-v310-forked-ouezc?file=/src/MapWrapper.jsx
3- Properties for search input: https://smeijer.github.io/leaflet-geosearch/leaflet-control