Leaflet and React (Map container not found) On Render - javascript

I have a React and Leaflet.js component...
export const MapSetLocationComponent = (props) => {
React.useEffect(() => {
let marker = null
let map = null
var container = L.DomUtil.get('setLocationMap')
if (container != null) {
container._leaflet_id = null
}
map = L.map('setLocationMap', {
center: [51.4556852, -0.9904706],
zoom: 16,
layers: [L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {})]
})
map.on('click', function (e) {
if (marker) map.removeLayer(marker)
marker = L.marker([e.latlng.lat, e.latlng.lng], { title: 'My marker' }).addTo(map)
})
})
return (
<>
<h3>Place your pin</h3>
<div
id="setLocationMap"
style={{
width: '100%',
height: '300px'
}}
/>
</>
)
}
Sometimes when I load the parent component I get the following error
'Map container not found.'
Which would indicate that the is somehow not available, because if I comment out that div I get the same error.
Anybody got any ideas? Do I need to use a different hook?

Indeed you should use the useRef hook to access the actual DOM element (once it is available) and pass it to Leaflet L.map factory.
function myComponent(props) {
const mapRef = useRef(null);
useEffect(() => {
if (mapRef && mapRef.current) {
L.map(mapRef.current); // ref.current is the actual DOM element
}
}, [mapRef]); // re-evaluate once the ref is available
render (<div ref={mapRef} />)
}
That is how React Leaflet, mentionned by #pilchard, works under the hood (simplified).
As pointed out by #MattMorgan, your rendered template may not be inserted in the actual DOM tree initially, hence your DOM query may fail. As a general rule, refrain from directly fiddling with DOM that React is supposed to manage.

Related

Use google-map-react fitbounds useRef -> mapRef.current.fitBounds is not a function

I use google-map-react and I want to have the correct zoom and center to the map.
To center the map I use the props center on GoogleMapReact component that I can calculate.
But for the zoom it is more complex.
I could see the use of fitBounds but that is not available on "google-map-react"
Is there a way to calculate the zoom so that it contains the markers that I have defined.
Or a function that does it by itself like fitbounds ?
Be careful not to confuse "google-map-react" (used here) with "react-google-map" (not used)
import GoogleMapReact from "google-map-react";
import {Place} from "#material-ui/icons";
import {useRef} from "react";
const Page = () => {
const mapRef = useRef(null);
const MarkerPlace = (props) => {
return (
<div>
<Place fontSize={"large"}></Place>
<p>{props.name}</p>
</div>
)
}
const FitBounds = () => {
let bounds = new google.maps.LatLngBounds();
let lat = 38.103;
let lng = -121.572;
bounds.extend(new google.maps.LatLng(lat, lng));
console.log(mapRef.current) //o {props: {…}, context: {…}, refs: {…}, updater: {…}, _getMinZoom: ƒ, …}
//The error occurs at the line below
mapRef.current.fitBounds(bounds) //TypeError: mapRef.current.fitBounds is not a function
//The error occurs at the line above
}
return (
<div>
<GoogleMapReact ref={mapRef} bootstrapURLKeys={{key: ""}} center={defaultCenter} defaultZoom={13}
{view === "recherche" && currentSearchData.map((activity, index) => (
<MarkerPlace key={index} lat={activity.latitude} lng={activity.longitude} name={activity.name}/>
))}
</GoogleMapReact>
<button onclick={FitBounds}>FitBounds</button>
</div>
)
}
export default Page;
fitBounds does exist in google-map-react, but it's a separate utility function rather than a function on the instance. There are examples of how to use it in the api. According to the source code, fitBounds is defined to take corner boundaries as the first parameter, and a height/width dictionary as the second:
export function fitBounds({ nw, se, ne, sw }, { width, height }) {
and it returns a new object with newBounds, presumably to be destructured and used to set the new center and zoom.

Update tileLayer url in react-leaflet

So I got this code running to render a leaflet ok, trying to replace the url whenever the colorMode changes is the challenge here.
useEffect is triggered ok displaying the correct variable but I can't update that TileLayer in any way.
export const Map = () => {
const { colorMode } = useColorMode();
let state = { center: { lat: 51.505, lng: -0.09 }, zoom: 13 };
const colorModeUrl = ['https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png']
useEffect(() => {
console.log(colorMode);
}, [colorMode]);
return (
<MapContainer
center={state.center}
zoom={state.zoom}
style={{ height: '100%' }}>
<TileLayer url={colorMode === 'light' ? colorModeUrl[0] : colorModeUrl[1]} />
</MapContainer>
)
}
Looking at the TileLayer documentation, the url prop is not mutable. After the initial render the component will not update if the prop is changed:
However, you can add a ref to the layer and update the url that way
export const Map = () => {
const ref = useRef(null);
const state = { center: { lat: 51.505, lng: -0.09 }, zoom: 13 };
const { colorMode } = useColorMode();
const light = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const dark =
"https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png";
};
useEffect(() => {
if (ref.current) {
ref.current.setUrl(colorMode === "light" ? light : dark);
}
}, [colorMode]);
return (
<div>
<MapContainer
center={state.center}
zoom={state.zoom}
style={{ height: "100%" }}
>
<TileLayer ref={ref} url={colorMode === "light" ? light : dark} />
</MapContainer>
</div>
);
};
https://codesandbox.io/s/react-leaflet-forked-1534x?file=/src/index.js:0-1158
I encountered a similar issue today trying to change the TileLayer using react-leaflet v4. As mentioned in the accepted answer, the immutability of the TileLayer url attribute is the problem. My dead-simple workaround was to pass a "key" attribute to TileLayer that will change whenever a new tile source is commanded. This will force a remount of the TileLayer component.
Hopefully this isn't subtly dangerous or an antipattern, but it seems to work with minimal complexity.

How to open popup in react-leaflet?

I am working on developing a react-leaflet map that has multiple markers. My intention is to make popups open one by one with interval.
So, my markers are rendered by mapping an array and returning component
arr.map((item, i) => <CustomMarker isActive={isActive} data={item} key={i} />)
isActive is a value that says should popup be active.
<CustomMarker /> looks like this:
const CustomMarker = ({isActive, data}) => {
return (
<Marker position={data.position}>
<Popup>
<a href={data.url}>Go to this website</a>
</Popup>
</Marker>
)
}
I tried several things but none do work for me.
Implement through eventHandlers
Create custom icon using L.divIcon() and write HTML with custom popup. However, popup was unclickable as it was a part of markers icon.
How can use isActive value to open popup?
You can do this by getting a ref to the react-leaflet Popup component, and once that ref is ready, using it to call the openOn method of an L.popup.
First you need a ref to the map that can be passed to each CustomMarker:
const Map = (props) => {
const [map, setMap] = useState();
return (
<MapContainer
whenCreated={setMap}
{...otherProps}
>
<CustomMarker
map={map}
data={{
position: [20.27, -157]
}}
/>
<CustomMarker
map={map}
data={{
position: [20.27, -156]
}}
isActive
/>
</MapContainer>
);
};
As you can see, each CustomMarker takes the map reference as a prop. Then within the CustomMarker, you need to get a ref to the Popup component. Note that a ref will not be available on mount. You need to wait for the ref to be available. But because useEffect's dont cause rerenders when a ref changes value, you can let the component know the ref is ready by setting a state variable in a ref callback:
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
popupRef.openOn(map);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
Yupperz
</Popup>
</Marker>
);
};
The ref is only available once the component mounts. In the ref prop, first we set the ref to our useRef value, and then we change the state variable refReady to be true. The useEffect fires when this value changes. The if statement assures that the ref indeed exists. If the ref is ready, and isActive is true, the we call L.popup.openOn(map), and your popup is open on mount.
Working codesandbox
Or, if you don't want to bother with all that, ths functionality (and more!) is built in to a react-leaflet-editable-popup.

Best way of sending request to child component

I am creating a map application that consists of a map (react-map-gl) showing GeoJSON markers and a simple table that lists the properties of the same markers.
Both the map component and the table component are direct children of a common component (the main App component). When clicking a marker in the table, I want the map component to zoom to this marker.
For the map to start a transition, I need to set its state.viewport like shown in this example:
https://visgl.github.io/react-map-gl/docs/advanced/viewport-transition
If you move the "New York City" button in that example to a sister component instead, you have my case exactly.
How do I send a map transition request from a sister component to the map component in the "React way"?
The React way is to have a common parent that will handle all state related actions, that is called lifting your state up. The idea is to always keep a single source of truth, let it go down the tree via props and use function to update the state.
In your case, a single exemple could look like this:
const defaultViewport = viewport: {
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14,
}
// The app will handle the viewport and related update
const App = () => {
const [viewport, setViewport]= useState(defaultViewport);
const handleTransition = (transitionViewport) => {
setViewport({
...viewport,
...transitionViewport
});
}
const handleViewportChange = (newViewport) => {
setViewport(newViewport);
}
return (
<div>
<Map
viewport={viewport}
onViewportChange={handleViewportChange}
/>
<Menu
onTransition={handleTransition}
/>
</div>
);
}
// The map only need to be aware of the viewport and how to change it
const Map = ({ viewport, onViewportChange }) => {
return (
<ReactMapGL
{...viewport}
onViewportChange={onViewportChange}
/>
);
}
// The menu only need to know how to request a transition
const Menu = ({onTransition}) => {
const goToNYC = () => {
const viewport = {
longitude: -74.1,
latitude: 40.7,
zoom: 14,
transitionDuration: 5000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: d3.easeCubic
};
onTransition(viewport);
}
return (
<div>
<button onClick={goToNYC}>New York City</button>
<div>
);
}
You'll need a parent component that will handle the state changes and then send the state and handler as props to the child components.
For example below, you have a parent component called MyApp which has two child components, Map which contains the map and NYCButton which contains the button that triggers the state change.
The MyApp component...
import React, { useState } from "react";
import { FlyToInterpolator } from "react-map-gl";
import d3 from "d3-ease";
import { Map } from "./Map";
import { NYCButton } from "./NYCButton";
const MyApp = () => {
const [viewport, setViewport] = useState({
width: 800,
height: 600,
longitude: -122.45,
latitude: 37.78,
zoom: 14
});
const goToNYC = () => {
setViewport({
...viewport,
longitude: -74.1,
latitude: 40.7,
zoom: 14,
transitionDuration: 5000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: d3.easeCubic
});
};
return (
<div className="MyApp">
<Map viewport={viewport} onViewportChange={setViewport} />
<NYCButton goToNYC={goToNYC} />
</div>
);
};
export { MyApp };
The Map component...
mport React from "react";
import ReactMapGL from "react-map-gl";
const Map = ({ viewport, onViewportChange }) => (
<div>
<ReactMapGL {...viewport} onViewportChange={onViewportChange} />
</div>
);
export { Map };
The NYCButton component...
import React from "react";
const NYCButton = ({ goToNYC }) => (
<div>
<button onClick={goToNYC}>New York City</button>
</div>
);
export { NYCButton };

Slow Performance for filtering markers in react-leaflet

I need an advice for the react-leaftlet port of leaflet. I am generating markers on a map and use marker clustering with react-leaflet-markercluster. Each markerdata is associated with some data. I want to filter that data based on the markers in the viewport.
My idea: Get the boundaries of the map and cross-check with each markers. Yes, it works. But the performance is extremly slow (> 4.5secs for calculating), when adding more than 500 markers.
What can I do to increase the performance?
Here is my code:
import React, { Component, Fragment } from 'react';
import CustomMarkers from './components/CustomMarkers';
import { Map, TileLayer } from 'react-leaflet';
import ImageContainer from './components/ImageContainer';
import { checkIfMarkerOnMap, createSampleData } from './utils/helpers';
import L from 'leaflet';
class App extends Component {
constructor(props){
super(props);
this.state = {
viewport: {
width: '100%',
height: '400px',
latitude: 40.00,
longitude: 20.00,
zoom: 5
},
visibleMarkers: {},
markers : {},
}
}
componentDidMount = () => {
const sampleData = createSampleData(1000);
this.setState({ markers: sampleData, visibleMarkers: sampleData });
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
this.setState({ mapBoundaries });
}
getMapBoundaries = () => {
// Get map boundaries
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
if(this.state.mapBoundaries !== mapBoundaries){
console.log("different");
this.setState({ mapBoundaries } );
} else return;
}
checkVisibleMarkers = () => {
console.time("checkVisibleMarkers");
const { markers, mapBoundaries } = this.state;
let visibleMarkers = Object.keys(markers)
.filter(key => (L.latLngBounds([[mapBoundaries._southWest.lat, mapBoundaries._southWest.lng], [mapBoundaries._northEast.lat, mapBoundaries._northEast.lng]]).contains([markers[key].coordinates.latitude,markers[key].coordinates.longitude])))
.map(key => { return { [key] : markers[key] } });
visibleMarkers = Object.assign({}, ...visibleMarkers);
console.log("visibleMarkers", visibleMarkers);
// this.setState({ visibleMarkers })
console.timeEnd("checkVisibleMarkers");
}
handleViewportChanged = () => {
this.getMapBoundaries();
this.checkVisibleMarkers();
}
render() {
console.log("this.mapRef", this.mapRef);
const { viewport, markers, visibleMarkers } = this.state;
const position = [viewport.latitude, viewport.longitude]
return (
<Fragment>
<Map
ref={(ref) => { this.mapRef = ref }}
center={position}
zoom={viewport.zoom}
maxZoom={15}
onViewportChanged={() => this.handleViewportChanged()}
style={{ height: '400px' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
/>
<CustomMarkers visibleMarkers={markers} />
</Map>
{/* <ImageContainer visibleMarkers={visibleMarkers} /> */}
</Fragment>
)
}
}
export default App;
CustomMarker.js:
import React, { Component } from 'react';
import { Marker, Tooltip } from 'react-leaflet';
import uuid from 'uuid-v4';
import {
heartIcon,
heartIconYellow,
heartIconLightblue,
heartIconDarkblue } from './../icons/icons';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import L from 'leaflet';
class CustomMarkers extends Component {
render() {
const { visibleMarkers } = this.props;
let markers;
if(Object.keys(visibleMarkers).length > 0) {
markers = Object.keys(visibleMarkers).map(key => {
let latitude = visibleMarkers[key].coordinates.latitude;
let longitude = visibleMarkers[key].coordinates.longitude;
let icon = heartIcon;
if(visibleMarkers[key].category === 'fb') icon = heartIconLightblue;
if(visibleMarkers[key].category === 'blogs') icon = heartIconYellow;
if(visibleMarkers[key].category === 'artisan') icon = heartIcon;
if(visibleMarkers[key].category === 'website') icon = heartIconDarkblue;
return (
<Marker
key={uuid()}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>
)
});
}
const createClusterCustomIcon = (cluster) => {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'marker-cluster-custom',
iconSize: L.point(40, 40, true),
});
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
disableClusteringAtZoom={10}
zoomToBoundsOnClick={true}
spiderfyOnMaxZoom={false}
removeOutsideVisibleBounds={true}
maxClusterRadius={150}
showCoverageOnHover={false}
>
{markers}
</MarkerClusterGroup>
)
}
}
export default CustomMarkers;
createSampleData takes the amount of sample data to generate as an input and creates a json structure for sample data { id: 1 { coordinates: {},...}
The bottleneck is the function checkVisibleMarkers. This function calculates if the marker is in viewport. Mathimatically its just two multiplications per marker.
I see a few potential issues - the performance of the checkVisibleMarkers function, and the use of uuid() to create unique (and different) key values on each rerender of a <Marker />.
checkVisibleMarkers
Regarding the checkVisibleMarkers function. There's a few calls and patterns in there which could be optimized. Here's what's currently happening:
Create an array of the markers keys
Loop through the keys, reference the corresponding marker and filter by location using L.latLngBounds().contains()
Loop through the filtered keys to create an array of objects as {key: marker}
Use Object.assign() to create an object from the array of objects
In the end, we have an object with each value being a marker.
I'm unsure of the internals of L.latLngBounds but it could be partly responsible for the bottleneck. Ignoring that, I'll focus on refactoring the Object.assign({}, ...Object.keys().filter().map()) pattern using a for...in statement.
checkVisibleMarkers = () => {
const visibleMarkers = {};
const { markers, mapBoundaries } = this.state;
for (let key in markers) {
const marker = markers[key];
const { latitude, longitude } = marker.coordinates;
const isVisible = mapBoundaries.contains([latitude, longitude]);
if (isVisible) {
visibleMarkers[key] = marker;
}
}
this.setState({ visibleMarkers });
}
A quick check on jsPerf shows the above method is ~50% faster than the method you're using, but it doesn't contain the L.latLngBounds().contains() call so it's not an exact comparison.
I also tried a method using Object.entries(markers).forEach(), which was slightly slower than the for...in method above.
The key prop of <Marker />
In the <Marker /> component you're using uuid() to generate unique keys. While unique, each rerender is generating a new key, and every time a component's key changes, React will create a new component instance. This means every <Marker /> is being recreated on every rerender.
The solution is to use a unique, and permanent, key for each <Marker />. Thankfully it sounds like you already have a value that will work for this, the key of visibleMarkers. So use this instead:
<Marker
key={key}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>

Categories