My web-app, for some reason, isn't fully updating when the state changes.
Let me explain: I have a React-Leaflet Map that I wish to update anytime a user changes filtering criteria. For instance, changing the city where some Markers
appear. For this purpose I've built a backend that loads some JSON from an endpoint, my web-app fetches it, and then successfully updates the items array where I store the data I need. After this, the new Markers are indeed added to the Map.
What is not updated is the zoom
and the center
of my MapContainer
, despite the functions that take care of this are correctly executed when the component is mounted.
So, in short, inside componentDidMount
I fetch data which is then passed to the state
which is used to populate and render my Map. After that, if the user presses the filter button and inserts a new city my componentDidUpdate
recognizes that props
have changed, it then fetches new data. Still, my Map only re-renders adding the new Markers without setting the new zoom and the new center.
Would anyone be so kind as to help me on this one? Huge thanks in advance.
import React from "react";
import { MapContainer, TileLayer } from "react-leaflet";
import MyMarker from "./MyMarker";
import "./MapObject.css";
/*
IMPORTANT: We are NOT taking into consideration the earth's curvature in
neither getCenter nor getZoom. This is only left as it is for
the time being because it is not mission critical
*/
// Function to calculate center of the map based on latitudes and longitudes in the array
function getCenter(json) {
// Array to store latitude and longitude
var lats = [];
var lngs = [];
const arr = Object.keys(json).map((key) => [key, json[key]]);
// Loop through the array to get latitude and longitude arrays
for (let i = 0; i < arr.length; i++) {
lats.push(arr[i][1].Latitude);
lngs.push(arr[i][1].Longitude);
}
const lat = lats.reduce((a, b) => a + b) / lats.length;
const lng = lngs.reduce((a, b) => a + b) / lngs.length;
return [lat, lng];
}
// Function to get the zoom level of the map based on the number of markers
function getZoom(json) {
// Array to store latitude and longitude
var lats = [];
var lngs = [];
const arr = Object.keys(json).map((key) => [key, json[key]]);
// Loop through the array to get latitude and longitude arrays
for (let i = 0; i < arr.length; i++) {
lats.push(arr[i][1].Latitude);
lngs.push(arr[i][1].Longitude);
}
const zoom = Math.floor(Math.log2(lats.length)) + 8;
return zoom;
}
export default class MapObject extends React.Component {
constructor(props) {
super(props);
this.state = {
map: null,
dataIsLoaded: false,
zoom: null,
position: [null, null],
items: [],
};
}
changePos(pos) {
this.setState({ position: pos });
const { map } = this.state;
if (map) map.flyTo(pos);
}
fetchData(filter, param) {
fetch(`https://my-backend-123.herokuapp.com/api/v1/${filter}/${param}`)
.then((res) => res.json())
.then((json) => {
this.setState(
{
items: json,
dataIsLoaded: true,
zoom: getZoom(json),
position: [getCenter(json)[0], getCenter(json)[1]],
},
() => {
console.log("State: ", this.state);
this.changePos(this.state.position);
}
);
});
console.log(
"Fetched new data: " +
"DataisLoaded: " +
this.state.dataIsLoaded +
" " +
"Zoom: " +
this.state.zoom +
" " +
"Lat: " +
this.state.position[0] +
" " +
"Lng: " +
this.state.position[1]
);
}
componentDidMount() {
this.fetchData("city", this.props.filterValue);
}
componentDidUpdate(prevProps) {
if (prevProps.filterValue !== this.props.filterValue) {
this.fetchData("city", this.props.filterValue);
//MapContainer.setCenter([getCenter(this.state.items)[0], getCenter(this.state.items)[1]]);
}
}
render() {
// Logic to show skeleton loader while data is still loading from the server
const { dataIsLoaded, items } = this.state;
console.log("Rendering!");
if (!dataIsLoaded)
return (
<div className="bg-white p-2 h-160 sm:p-4 rounded-2xl shadow-lg flex flex-col sm:flex-row gap-5 select-none">
<div className="h-full sm:h-full sm:w-full rounded-xl bg-gray-200 animate-pulse"></div>
</div>
);
// Logic to show map and markers if data is loaded
return (
<div>
<MapContainer
id="mapId"
whenCreated={(map) => this.setState({ map })}
attributionControl={true} // remove Leaflet attribution control
center={this.state.position}
zoom={this.state.zoom}
scrollWheelZoom={false}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{this.state.map}
{items.map((item, index) => (
<div key={index}>
<MyMarker
name={item.Name}
city={item.City}
prov={item.Province}
lat={item.Latitude}
lng={item.Longitude}
phone={item["Phone Number"]}
/>
</div>
))}
</MapContainer>
</div>
);
}
}
I'm leaving a GIF down below to showcase the current behavior. You can also briefly notice that the Markers "disappear": that's because the new Markers have been rendered taking into account the new filter, that's why I then proceed to manually zooming out to show that the new Markers have been indeed rendered.
EDIT: I am now trying to implement this solution, but I’ve been unsuccessful so far.
Alrightie, I decided to fully refactor my code and to switch to a function
component, from the class
component I had earlier.
The solution I've implemented comes from this answer.
The key was to add the following function to my code:
/*
Function to move the map to the center of the markers after the map
is loaded with new data and with the correct zoom.
*/
function FlyMapTo(props) {
const map = useMap();
useEffect(() => {
map.flyTo(props.center, props.zoom);
});
return null;
}
After that, I add this component to the JSX that takes care of rendering the parent component (my Map.js
):
return (
<MapContainer center={center} zoom={zoom} scrollWheelZoom={true}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{items.map((item, index) => (
<div key={index}>
<MyMarker
name={item.Name}
city={item.City}
prov={item.Province}
lat={item.Latitude}
lng={item.Longitude}
phone={item["Phone Number"]}
/>
</div>
))}
<FlyMapTo center={center} zoom={zoom} />
</MapContainer>
);
Just for reference, should anyone face a similar problem, I'm adding the full code of my new Map.js
component:
import React, { useState, useEffect } from "react";
import "./MapObject.css";
import { MapContainer, TileLayer } from "react-leaflet";
import MyMarker from "./MyMarker";
import { useMap } from "react-leaflet";
/*
IMPORTANT: We are NOT taking into consideration the earth's curvature in
neither getCenter nor getZoom. This is only left as it is for
the time being because it is not mission critical
*/
// Function to calculate center of the map based on latitudes and longitudes in the array
function getCenter(json) {
// Array to store latitude and longitude
var lats = [];
var lngs = [];
const arr = Object.keys(json).map((key) => [key, json[key]]);
// Loop through the array to get latitude and longitude arrays
for (let i = 0; i < arr.length; i++) {
lats.push(arr[i][1].Latitude);
lngs.push(arr[i][1].Longitude);
}
const lat = lats.reduce((a, b) => a + b) / lats.length;
const lng = lngs.reduce((a, b) => a + b) / lngs.length;
return [lat, lng];
}
// Function to get the zoom level of the map based on the number of markers
function getZoom(json) {
// Array to store latitude and longitude
var lats = [];
var lngs = [];
const arr = Object.keys(json).map((key) => [key, json[key]]);
// Loop through the array to get latitude and longitude arrays
for (let i = 0; i < arr.length; i++) {
lats.push(arr[i][1].Latitude);
lngs.push(arr[i][1].Longitude);
}
const zoom = Math.floor(Math.log2(lats.length)) + 8;
return zoom;
}
// Function to move the map to the center of the markers after the map is loaded with new data
function FlyMapTo(props) {
const map = useMap();
useEffect(() => {
map.flyTo(props.center, props.zoom);
});
return null;
}
export default function Map(props) {
const [zoom, setZoom] = useState(null);
const [center, setCenter] = useState([null, null]);
const [items, setItems] = useState([]);
const [dataIsLoaded, setDataIsLoaded] = useState(false);
useEffect(() => {
fetch(
`https://my-backend-123.herokuapp.com/api/v1/${props.filterType}/${props.filterValue}`
)
.then((res) => res.json())
.then((json) => {
if (json["detail"] === "Not Found") {
setItems([]);
setDataIsLoaded(false);
} else {
setItems(json);
setDataIsLoaded(true);
setCenter(getCenter(json));
setZoom(getZoom(json));
}
});
}, [props.filterType, props.filterValue]);
// If data is not loaded, show an empty map
if (!dataIsLoaded && items.length === 0) {
return (
<MapContainer
center={{ lat: 45.0, lng: 12.0 }}
zoom={5}
scrollWheelZoom={true}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
);
// If data is loaded, show the map with markers
} else if (dataIsLoaded) {
return (
<MapContainer center={center} zoom={zoom} scrollWheelZoom={true}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{items.map((item, index) => (
<div key={index}>
<MyMarker
name={item.Name}
city={item.City}
prov={item.Province}
lat={item.Latitude}
lng={item.Longitude}
phone={item["Phone Number"]}
/>
</div>
))}
<FlyMapTo center={center} zoom={zoom} />
</MapContainer>
);
}
}