Update tileLayer url in react-leaflet - javascript

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.

Related

How do I make an infowindow appear when the user clicks the marker, while using a functional component?

The Google Maps API library I am using for this is https://github.com/JustFly1984/react-google-maps-api
What I have been asked to create is a map with multiple markers, each of which can be clicked on to open an InfoWindow centred on that marker. Much like the AirBnB search page, with the map on one side results on the other.
However, I seem to only be able to have an InfoWindow that is open at the loading of the page and then can't be opened again.
My research so far has generally shown that I would need to completely redo this component as a class instead. Although I don't really understand the class based model of components, so ideally I wouldn't have to mess around with that.
Is there a way to make the desired effect without reworking my program entirely?
This is the code for the component, in case it is relevant
import styles from '../../styles/Home.module.css';
import { Marker, LoadScriptNext, GoogleMap, InfoWindow } from '#react-google-maps/api';
import { Fragment, React } from 'react';
import { Typography } from '#material-ui/core';
export default function MapPanel(props) {
return (
<Fragment>
<LoadScriptNext id='google-map-example' googleMapsApiKey="MYAPIKEY">
<GoogleMap mapContainerStyle={{ width: "100%", height: "800px" }}
id='google-map'
center={props.centre}
heading="0"
zoom={12}>
{addMarkers(props.markerList)}
</GoogleMap>
</LoadScriptNext>
</Fragment>
);
}
function addMarkers(markerList) {
if (markerList != undefined) {
return (markerList.map((marker, index) => makeMarker(marker, index)))
}
}
function makeMarker(marker, index) {
const handleClick = () => {
console.log(`${marker.title} marker clicked`);
}
return (
<Marker id={index} position={marker.latLong} title={marker.title} onClick={handleClick}>
<InfoWindow id='Test' position={marker.latLong}>
<Typography variant='body2'>{marker.text}</Typography>
</InfoWindow>
</Marker>
);
}
To achieve your goal you would want to use states in your application. But since you want to deal with it using Function components, you would need to make use of react hooks to use states.
Here is a working sample for your reference: https://stackblitz.com/edit/marker-with-infowindow-react-api-o6357y
import ReactDOM from "react-dom";
import React from "react";
import {
GoogleMap,
Marker,
InfoWindow,
LoadScript
} from "#react-google-maps/api";
import data from "./data.json";
const { useState } = React;
const containerStyle = {
width: "400px",
height: "400px"
};
const center = { lat: 40.712775, lng: -74.005973 };
function Map() {
const [infoWindowID, setInfoWindowID] = useState("");
let markers;
if (data !== null) {
markers = data.map((location, i) => {
const marker = { lat: location.lat, lng: location.lng };
const index = i + 1;
return (
<Marker
key={index}
position={marker}
label={index.toString()}
onClick={() => {
setInfoWindowID(index);
}}
>
{infoWindowID === index && (
<InfoWindow>
<span>Something {index}</span>
</InfoWindow>
)}
</Marker>
);
});
}
return (
<LoadScript googleMapsApiKey="YOUR_API_KEY">
<GoogleMap mapContainerStyle={containerStyle} center={center} zoom={10}>
{markers}
</GoogleMap>
</LoadScript>
);
}
export default React.memo(Map);

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

react-google-maps/api Avoiding re-render of Map after some state changes

I was having problems where my GoogleMaps instance would refresh and self center itself on some onClick function where the state was being set and the entire Component rendering cycle would happen.
After some Googling it was suggested that the component instantiation be separated and re-used. The problem now is I have some logic to display markers inside <GoogleMaps> component that is no longer working as expected and I don't know how to refactor:
export default function LocationSearchResults({
...
}) {
const [map, setMap] = useState(null)
const [markersContainer, setMarkersContainer] = useState([])
const getMap = () => {
if (map) {
return map;
} else {
setMap(<GoogleMap mapContainerStyle={containerStyle}
options={ {
minZoom: 3,
maxZoom: 15
}}
center={{
lat: 49.25,
lng: -84.5
}}
zoom={5}
onLoad={onLoad}
onDragEnd={onDragEnd} >
{
markersContainer.map(place => { //Only executes once? Does not listen for changes
return (< Marker key={place.id}
position={ place.position}
/>
)
})
}
</GoogleMap>
)
return map
}
}
render( <div className="..." >
{
getMap()
}
</div>
)
}
I don't have a ton of experience with React, any help is appreciated thanks!
I set up my component instantiation like so using useMemo
...instantiate all event listener functions here
const map = useMemo(() =>
{
return (<GoogleMap
mapContainerStyle={containerStyle}
options={{ minZoom: 3, maxZoom: 15 }}
center={{
lat: 49.25,
lng: -84.5
}
}
zoom={5}
onLoad={onLoad}
onDragEnd={onDragEnd}
// onUnmount={onUnmount}
>
{markersContainer.map(place => { return ( <Marker
key={place.id}
position={place.position} />
)
})
}
</GoogleMap>)
}, [markersContainer])
then I simply render in my render() function:
return (
<>
<div>...
{map}
</div>
</>)
No more unnecessary refreshes unless new markers are added/removed.

How to pass coordinates data from a class to another class in React

I want to pass coordinates from a class to another one. I am retrieving coordinates from an api. The code looks like the following:
import React, { Component } from 'react';
class Test3 extends Component{
state = {
loading: true,
coordinates: null,
}
async componentDidMount(){
const url = "https://ttr.vsbbn.nl:4000/gps_history?team_id=10";
const response = await fetch(url);
const data = await response.json();
this.setState({coordinates: data, loading: false });
}
render(){
const { loading, coordinates } = this.state
return(
<div>
{loading || !coordinates ? (
<div>loading...</div>
) : (
<div>
{coordinates.map((coordinate, index) => {
return (
<div key={index}>
<p>Longitute: {coordinate.lon}</p>
<p>Latitude: {coordinate.lat}</p>
<p>Time: {coordinate.timestamp}</p>
<p>...............</p>
</div>
)
})}
</div>
)}
</div>
)
}
}
export default Test3;
And i want to pass the coordinates to the class here below. I already have the path hardcoded in an array. The api coordinates supposed to be in the path array, thats my goal. The code is here:
import React from 'react';
import {
GoogleMap,
withScriptjs,
withGoogleMap,
Marker,
Polyline}
from 'react-google-maps';
import '../App.css';
import { Link } from "react-router-dom";
class Map extends React.Component {
path = [
{ lat: 18.558908, lng: -68.389916 },
{ lat: 18.558853, lng: -68.389922 },
{ lat: 18.558375, lng: -68.389729 },
{ lat: 18.558032, lng: -68.389182 },
{ lat: 18.55805, lng: -68.388613 },
{ lat: 18.558256, lng: -68.388213 },
{ lat: 18.558744, lng: -68.387929 }
];
render = () => {
return (
<GoogleMap
defaultZoom={16}
defaultCenter={{ lat: 18.559008, lng: -68.388881 }}
>
<Polyline path={this.path} options={{ strokeColor: "#FF0000 " }} />
<Marker position={this.path[this.path.length - 1]} />
</GoogleMap>
)
}
}
const WrappedMap = withScriptjs(withGoogleMap(Map));
export default() => (
<div>
<br/>
<b>Klik <Link to="/" className="btn btn-primary">hier</Link> om terug te gaan naar teamoverzicht</b>
<br/>
<br/>
<h3>Live kaart</h3>
<p>
Teamnaam: Hogeschool Rotterdam 1 <br/>
Live:<span class="dot"></span><br/>
Actuele snelheid: 8 km per uur <br/>
Gemiddelde snelheid: 7 km per uur <br/>
Verwachte aankomsttijd Rotterdam: 12 mei 17.59 <br/>
</p>
<div style={{width: '50vw', height: '50vh'}}>
<WrappedMap googleMapURL = {'https://maps.googleapis.com/maps/api/js?key=AIzaSyB9w0T_VMezCy1AaqxXpRie9ChrbVCt1O4&v=3.exp&libraries=geometry,drawing,places'}
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `100%` }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
</div>
</div>
)
Any idea how to do this? My goal is showing real time the location of a person.
Data passing in React is unidirectional, it means that only a parent can pass props to its child. So if you can nest your component "Map" into "Test3" that will solve it. If you can't do that, then you need to use Redux, which is state management library, that will handle complex data passing between components.
PS: there is way to pass data from a child to its parent, but its very limited (I can send you a tuto if you're interested)
According to this documentation, A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature
so after you read this doc we are here to answer all of your questions about it.
you need to save the coordinates in an ordered way (to your state) that you could pass it to the children components (in your case <Map />).
in the componentDidMount of your <Test3 /> component try this:
async componentDidMount(){
const url = "https://ttr.vsbbn.nl:4000/gps_history?team_id=10";
const response = await fetch(url);
const data = await response.json();
const coordinatesList = []
data.forEach(coor => {
coordinatesList.push({ lat: coor.lat, lng: coor.lng })
})
this.setState({coordinates: coordinatesList, loading: false });
}
this would result something like below:
coordinatesList: [
{ lat: someNumber, lng: someNumber },
{ lat: someNumber, lng: someNumber },
{ lat: someNumber, lng: someNumber },
...
]
after that inside of render function you can pass the coordinatesList state to your child component and use it, something like
<Map coorList={coordinatesList} />
no need to loop over coordinates! you need to do what ever you want inside the map component wityh all of your coordinates. (as a side note i'm thinking that you try to create markers of those coordinates, so you need to loop over coorList props that you've been received from your parent and return a <Marker /> component of each of that coordinates )
Another option could be use use react context/provider, which would serve as a container for your data and also any functions to update the data. Your context could be accessed from both components and gives you the same single source of data that redux gives you, without all the boilerplate code that goes with redux.
Create a provider component and a use context hook
import React, { createContext, useContext, useState, useEffect } from 'react'
const Context = createContext({})
// This is your provider
const MapDataProvider = props => {
const [mapData, setMapData] = useState([...your original coordinates])
useEffect(() => {
...your call to google
.then(data, setMapData(current => [...current, data])
}, [])
return (
<Context.Provider value={{coordinates: mapData}}>
{props.children}
</Context.Provider>
)
}
// This is your hook
const useMapDataContext = () => useContext(Context)
export { MapDataProvider, useMapDataContext }
Wrap the exported Provider around your root component
import { MapDataProvider } from './yourcontextfile.js'
const RootComponent = () => (
<MapDataProvider>
<Test3/>
<WrappedMap/>
...whatever, the structure here is not important
</MapDataProvider>
)
Then in any of your components where you need to access the data, you use the exported hook useMapDataContext
import { useMapDataContext } from './yourcontextfile.js'
const Test3 = () => {
// This is deconstructed from the value prop on the provider component
const { coordinates } = useMapDataContext()
... the rest of your component
}
I haven't compiled or tested any of this, it's just a quick example of how you could use a context component to have your data in one place, accessible from any other component, as long as it's inside the <MapDataProvider> component.
I'm not saying it's the right way to do it, it's just another way.

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