I'm building an application using react-leaflet and I recently updated all of my dependencies which brought on an error that I can't solve.
The error:
React has detected a change in the order of Hooks called by ForwardRef(ContainerComponent)
Removing the <MapContainer>
component from my app fixes the error, but I cannot seem to figure out where the ForwardRef
component is being rendered or why the order of hooks changes within it between renders.
This is my component:
const Map = ({ openModal }) => {
const [homeCoords, setHomeCoords] = useState([49.2, -123]);
const [bounds, setBounds] = useState({
lat_lower: 48.9,
lat_upper: 49.5,
lon_left: -123.8,
lon_right: -122.2
});
// Get the user's location with navigator.geolocation
useEffect(() => {
if(!window.navigator.geolocation) {
return;
}
window.navigator.geolocation.getCurrentPosition(
// success
(res) => {
setHomeCoords([res.coords.latitude, res.coords.longitude]);
},
// failure
() => {
console.error('Must allow pestlocations.com to access your location to use this feature.')
}
)
}, []);
// Helper function for BoundTracker component
const collectBounds = (e) => {
const bounds = e.target.getBounds();
const currLatLower = bounds._southWest.lat;
const currLatUpper = bounds._northEast.lat;
const currLonLeft = bounds._southWest.lng;
const currLonRight = bounds._northEast.lng;
setBounds({
lat_lower: currLatLower,
lat_upper: currLatUpper,
lon_left: currLonLeft,
lon_right: currLonRight
})
}
// Listen for dragging or zooming on map and update bounds
const BoundTracker = () => {
useMapEvents({
// Drag map
dragend: (e) => {
collectBounds(e);
},
// Zoom map
zoomend: (e) => {
collectBounds(e);
}
})
}
const HomeButton = () => {
const map = useMap();
return (
<div className="btn home" aria-disabled="false" onClick={() => {
map.panTo(homeCoords);
const bounds = map.getBounds();
setBounds({
lat_lower: bounds._southWest.lat,
lat_upper: bounds._northEast.lat,
lon_left: bounds._southWest.lng,
lon_right: bounds._northEast.lng
})
}}>
<AiFillHome />
</div>
)
}
return (
<>
<MapContainer className="Map" position={homeCoords} zoom={10}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<HomeButton />
<div className="btn info" onClick={() => openModal("Welcome")}><AiOutlineInfoCircle /></div>
<div className="btn legend" onClick={() => openModal("Legend")}><BsMap /></div>
<div className="search-btn" onClick={() => return}>Search This Area</div>
<PointClusters />
<BoundTracker />
</MapContainer>
</>
)
}
EDIT Here is my PointClusters component:
import { useState } from 'react';
import MarkerClusterGroup from 'react-leaflet-cluster';
import { Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import './PointClusters.css';
// For testing purposes only - - - - - - - - - - - - - - - - - -
const testPoints = require('../../../testPoints.json').features;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const PointClusters = () => {
// Initialize points as testpoints. ** CHANGE FOR PRODUCTION **
const [points, setPoints] = useState(testPoints);
const newicon = new L.divIcon({
className: "custom-marker",
html: "<span class='arrow'></span>"
});
const createClusterCustomIcon = function (cluster) {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'custom-marker-cluster',
iconSize: L.point(33, 33, true),
})
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
>
{
points.map((point, i) => {
const { name, address, type, numOfReports, url } = point.properties;
const coords = [point.geometry.coordinates[1], point.geometry.coordinates[0]];
return (
<Marker position={coords} icon={newicon} key={i}>
<Popup>
{
name === "None" ? null :
<>
<b>{name}</b>
<br />
</>
}
<strong>Address</strong>: {address}
<br />
<strong>Type</strong>: {type}
<hr />
At least <a href={url}>{numOfReports} reports</a> on bedbugregistry.com
</Popup>
</Marker>
)
})
}
</MarkerClusterGroup>
)
}
Since you only have the useState hook in PointClusters I assume the issue here is react-leaflet-cluster package. I know it did not have support för react-leaflet 4 when I wanted to use it. It now have a version 2.0.0 that should be compatible, however looking into the code they use hooks in the solution.
Since they did not support react-leaflet 4 when I needed it I decided to adapt the actual code and modify to work and to fit my needs. Below is that adaption:
import { createPathComponent } from "@react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };
type ClusterEvents = {
onClick?: LeafletMouseEventHandlerFn;
onDblClick?: LeafletMouseEventHandlerFn;
onMouseDown?: LeafletMouseEventHandlerFn;
onMouseUp?: LeafletMouseEventHandlerFn;
onMouseOver?: LeafletMouseEventHandlerFn;
onMouseOut?: LeafletMouseEventHandlerFn;
onContextMenu?: LeafletMouseEventHandlerFn;
};
// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/@types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
spiderfyOnMaxZoom?: boolean;
children: React.ReactNode;
size?: number;
icon?: ReactElement;
}
const createMarkerCluster = (
{
children: _c,
size = 30,
icon = <Circle size={size} />,
...props
}: LeafletMarkerClusterProps,
context: any
) => {
const markerIcons = {
default: <Circle size={size} />,
property: <Building size={size} />,
business: <BuildingStore size={size} />,
} as { [key in string]: ReactElement };
const clusterProps: ClusterType = {
iconCreateFunction: (cluster: any) => {
const markers = cluster.getAllChildMarkers();
const types = markers.reduce(
(
acc: { [x: string]: number },
marker: {
key: string;
options: { icon: { options: { className: string } } };
}
) => {
const key = marker?.key || "";
const type =
marker.options.icon.options.className || key.split("-")[0];
const increment = (key.split("-")[1] as unknown as number) || 1;
if (type in markerIcons) {
return { ...acc, [type]: (acc[type] || 0) + increment };
}
return { ...acc, default: (acc.default || 0) + increment };
},
{}
) as { [key in string]: number };
const typeIcons = Object.entries(types).map(([type, count], index) => {
if (count > 0) {
const typeIcon = markerIcons[type];
return (
<div key={`${type}-${count}`} style={{ display: "flex" }}>
<span>{typeIcon}</span>
<span style={{ width: "max-content" }}>{count}</span>
</div>
);
}
});
const iconWidth = typeIcons.length * size;
return createLeafletIcon(
<div style={{ display: "flex" }} className={"cluster-marker"}>
{typeIcons}
</div>,
iconWidth,
undefined,
iconWidth,
30
);
},
showCoverageOnHover: false,
animate: true,
animateAddingMarkers: false,
removeOutsideVisibleBounds: false,
};
const clusterEvents: ClusterType = {};
// Splitting props and events to different objects
Object.entries(props).forEach(([propName, prop]) =>
propName.startsWith("on")
? (clusterEvents[propName] = prop)
: (clusterProps[propName] = prop)
);
const instance = new (L as any).MarkerClusterGroup(clusterProps);
instance.on("spiderfied", (e: any) => {
e.cluster._icon?.classList.add(styles.spiderfied);
});
instance.on("unspiderfied", (e: any) => {
e.cluster._icon?.classList.remove(styles.spiderfied);
});
// This is not used at the moment, but could be used to add events to the cluster.
// Initializing event listeners
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
instance.on(clusterEvent, callback);
});
return {
instance,
context: {
...context,
layerContainer: instance,
},
};
};
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};
const LeafletMarkerCluster = createPathComponent(
createMarkerCluster,
updateMarkerCluster
);
const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
children,
...props
}) => {
const markerCluster = useMemo(() => {
return <LeafletMarkerCluster>{children}</LeafletMarkerCluster>;
}, [children]);
return <>{markerCluster}</>;
};
export default LeafletMarkerClusterWrapper;
I combine different types of markers and show each icon with the number in the cluster. You should be able to replace iconCreateFunction to fit your needs.
The createLeafletIcon look like this:
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
Another tip is to looking into using useMemo on props, that the system might otherwise see as "new", but the first order of action should make it work, then you can try to find props that cause rerenders. Best of luck and let me know if you have any questions regarding the implementation