currently, I'm displaying a single copy of the map. while panning at the edge of the graph or zooming out further from the makers changing their positions. I want to have the markers stay in their position even when these operations are done how can I achieve that.
codesanbox - https://codesandbox.io/s/mapbox-marker-issue-03cdc
index.js
import "mapbox-gl/dist/mapbox-gl.css";
import "react-map-gl-geocoder/dist/mapbox-gl-geocoder.css";
import React, { useState, useRef, useCallback } from "react";
import { render } from "react-dom";
import MapGL from "react-map-gl";
import Pins from "./pins";
import CITIES from "./cities.json";
const MAPBOX_TOKEN =
"pk.eyJ1Ijoic21peWFrYXdhIiwiYSI6ImNqcGM0d3U4bTB6dWwzcW04ZHRsbHl0ZWoifQ.X9cvdajtPbs9JDMG-CMDsA";
const App = () => {
const [viewport, setViewport] = useState({
latitude: 37.7577,
longitude: -122.4376,
zoom: 8
});
const mapRef = useRef();
const handleViewportChange = useCallback(
(newViewport) => setViewport(newViewport),
[]
);
return (
<div style={{ height: "100vh" }}>
<MapGL
ref={mapRef}
{...viewport}
width="90vw"
height="90vh"
onViewportChange={handleViewportChange}
mapboxApiAccessToken={MAPBOX_TOKEN}
mapOptions={{
renderWorldCopies: false,
maxBounds: [
[-180, -90], // Southwest coordinates
[180, 90] // Northeast coordinates
]
}}
>
<Pins data={CITIES} onClick={() => {}} />
</MapGL>
</div>
);
};
render(<App />, document.getElementById("root"));
pins.js
import * as React from "react";
import { Marker } from "react-map-gl";
const ICON = `M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
C20.1,15.8,20.2,15.8,20.2,15.7z`;
const SIZE = 20;
// Important for perf: the markers never change, avoid rerender when the map viewport changes
function Pins(props) {
const { data, onClick } = props;
return data.map((city, index) => (
<Marker
key={`marker-${index}`}
longitude={city.longitude}
latitude={city.latitude}
>
<svg
height={SIZE}
viewBox="0 0 24 24"
style={{
cursor: "pointer",
fill: "#d00",
stroke: "none",
transform: `translate(${-SIZE / 2}px,${-SIZE}px)`
}}
>
<path d={ICON} />
</svg>
</Marker>
));
}
export default React.memo(Pins);
data - https://github.com/visgl/react-map-gl/raw/master/examples/.data/cities.json
You have run into a limitation with react-map-gl: https://github.com/visgl/react-map-gl/issues/786
you cannot use maxBounds because react-map-gl is not using mapbox-gl-js's built-in interaction handling logic. To constrain the map bounds you should do something like
_onViewportChange = (viewport) => {
const oldViewport = this.state.viewport;
if (Math.abs(oldViewport.longitude - viewport.longitude) > 180) {
// wrapped around the 180th meridian
return;
}
this.setState({viewport});
}
As a workaround, you can remove the settings maxBounds and renderWorldCopies. Or follow the advice in that thread.
Related
Hey everyone so I'm learning React Native, using Expo VS code, and android studio
I was trying to get a location image with a pin using google maps.
But I cant see any map to pick my location at google maps in android studio.
Also, I cant see any marker after picking a location in iphone expo go google maps image.
What should I do?
Below is my code about getting image of map
export function getMapPreview(lat, lng) {
const imagePreviewUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=13&size=400x200&maptype=roadmap&markers=color:red%7Clabel:S%7C${lat},${lng}
&key=${GOOGLE_API_KEY}`;
return imagePreviewUrl;
}
Map screen
import { useCallback, useLayoutEffect, useState } from "react";
import { StyleSheet, Alert } from "react-native";
import MapView, { Marker } from "react-native-maps";
import IconButton from "../components/UI/IconButton";
export default function Map({navigation}) {
const [selectedLocation, setSelectedLocation] = useState();
const region = {
latitude: 37.56,
longitude: 126.97,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421
}
function selectLocationHandler(event) {
console.log(event);
const lat = event.nativeEvent.coordinate.latitude;
const lng = event.nativeEvent.coordinate.longitude;
setSelectedLocation({lat: lat, lng: lng})
}
const savePickedLocationHandler = useCallback(() => {
if (!selectedLocation) {
Alert.alert(
"No location picked",
'You have to pick a loacation (by tapping on the map) first!'
)
return;
}
navigation.navigate('AddPlace', { pickedLat: selectedLocation.lat, pickedLng: selectedLocation.lng })
}, [navigation, selectedLocation])
useLayoutEffect(() => {
navigation.setOptions({
headerRight: ({tintColor}) => (
<IconButton icon="save" size={24} color={tintColor} onPress={savePickedLocationHandler} />
)
})
}, [navigation, savePickedLocationHandler])
return (
<MapView style={styles.map} initialRegion={region} onPress={selectLocationHandler}>
{selectedLocation &&
<Marker
title="Picked Location"
coordinate={{latitude: selectedLocation.lat,longitude: selectedLocation.lng}}
/>
}
</MapView>
)
}
const styles = StyleSheet.create({
map: {
flex: 1
}
})
I reviewed my code several times, but I cant find why
Try use width and height to map styles
You need to define the width and height of the map.
const styles = StyleSheet.create({
map: {
width: MAP_WIDTH_VALUE,
height:MAP_HEIGHT_VALUE
}
})
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);
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 };
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.
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>