Dynamically add circles to react leaflet with blockchain - reactjs

I have read this post which partly answers my question, but my problem is the infinite loop that the provider.on method creates with setData(_data). I simply want to update the circle information to be rendered from my local blockchain, but the setData(_data) call creates an infinite loop.
I have tried using a global variable instead of using useState, which solves the infinite loop, but this updated value cannot be seen in other parts of the code.
function App() {
const initialPos = [55, 12];
const zoomLv = 13;
const [data, setData] = useState([])
const greeterAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
provider.on("block", async (blockNumber) => {
const _data = await contract.getCircle().split(", ") //[lat, lng, radius]
setData(_data)
});
function RenderCircle() {
if (data.length > 1) {
return <Circle center={[parseFloat(data[0]), parseFloat(data[1])]} radius={parseFloat(data[2])} />
}
return null
}
return (
<>
<MapContainer center={initialPos} zoom={zoomLv} id='map'>
<TileLayer
url="https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
attribution='© OpenStreetMap contributors'
maxZoom={20}
/>
<RenderCircle/>
</MapContainer>
</>
);
}

To solve it, I made it a react component class, and used its setState() instead of using useState(). This avoided the infinite loop.

Related

React dynamically added components not rendered

I'm dynamically adding instances of a custom (Kendo-React) component into an array in my main App.
The component:
const PersonDD = () => {
const ages = ["Child", "Adult", "Senior"];
return (
<div>
<div>Person:</div>
<DropDownList
data={ages} style={{ width: "300px", }}
/>
</div>
);
};
I'm adding one instance on initial render, and another two instances after the result from an Ajax call returns.
const SourceTab = (SourceTabProps) => {
....
var componentList = [];
componentList.push(<PersonDD/>);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
componentList.push(<PersonDD/>);
componentList.push(<PersonDD/>);
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList}
</div>);
};
The problem I have is that the one instance in the initial array are rendered, but the two created after the Ajax call are not rendered.
Do I need to call .render() or something similar to refresh?
You can simply use react useState to rerender component and in jsx map them.
like this :
const SourceTab = (SourceTabProps) => {
const [componentList,setComponentList] = useState([PersonDD])
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
console.log(res.data.item);
setComponentList([...componentList,PersonDD,PersonDD])
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{componentList.map((Component,index)=> <Component key={index} />)}
</div>);
};
You need to remember that React only re-renders (refreshes the UI/view) when a state changes. Your componentList is not a state at the moment but just an ordinary variable. make it a state by using useState hook.
Not sure if it is a bad practice or not but I haven't seen any react project that keeps an entire component as a state so instead of creating a state with an array of components, just push a data representation of the components you want to render. Then display the component list using your list and using .map
Here's how it would look like.
....
const [personList, setPersonList] = useState([1]);
async function getStrata(){
var url = '/access/.im.read';
const res = await axios.get( url );
setPersonList(state => state.push(2)); //you can make this dynamic so it can rerender as much components as you like, for now im pushing only #2
}
React.useEffect(() =>{
getStrata();
},[]);
return (
<Title title="People" />
<div className='assignment_div_css'>
{personList.map((item, key) => <PersonDD key={key} />)}
</div>);
};
Need to use the map to render a list
<div className='assignment_div_css'>
{componentList.map(component => <>{component}</>)}
</div>);
also, use a usestate to variable
const [componentList , setComponentList ]= React.useState[<PersonDD/>];
inside function set like this
console.log(res.data.item);
setComponentList(state => [...state, <PersonDD/>, <PersonDD/>]);

Stop Mapbox.js from re-mounting on every state change?

I am running into a particularly painful issue utilising react/redux/mapbox and was hoping to get some advice. I have a container component which houses both a Map component and a destination bar component which shows information about the route. I also need to expose the mapbox instance to the window object when it is available.
The problem I seem to be having is that when I render the Map component, i need to wait for the mapbox.load event, at which point I can then set the map instance to either a useState or useRef for later use in the code. If I set it as state then the map enters into a endless loop where it sets the state, re-loads the map and tries to set the state again. If I set it as a ref, then it wont re-render any of the details on the other component. Also if I were to use the window.map instance at any point it also re-mounts the Map component and starts this whole process off again.
function App() {
const { data, isError } = useConfigEndpointQuery();
const { route, to, from, routingDestinationData, routingOverviewData } =
useAppSelector((state) => state.routing);
const modals = useAppSelector((state) => state.ui.modals);
const mapInstance = useRef<LivingMap>();
const [isUserActive, setIsUserActive] = useState(false);
const [mode, setMode] = useState(ComponentMode.OVERVIEW);
const [isStaticUserLocationChevron, setIsStaticUserLocationChevron] =
useState(false);
const [countdownTimeInSeconds, setCountdownTimeInSeconds] = useState(
INITIAL_COUNTDOWN_TIME / 1000
);
const countdownInterval = useRef<NodeJS.Timer | undefined>();
const userActiveInterval = useRef<NodeJS.Timer | undefined>();
const setChevronInterval = useRef<NodeJS.Timer | undefined>();
const userLocationControl = useRef<UserLocationControl>();
const routingControl = useRef<RoutingControl>();
const geofenceControl = useRef<GeofenceControl>();
const floorControl = useRef<FloorControl>();
return data ? (
<div className={styles.container}>
<TopBar distance={totalLength} time={totalTime} />
<Map
bearing={data.areas[0].view.bearing}
zoom={data.areas[0].view.zoom}
maxZoom={data.areas[0].view.maxZoom}
minZoom={data.areas[0].view.minZoom}
center={data.areas[0].view.center}
extent={data.areas[0].view.extent}
floor={data.floors.find((floor) => !!floor.default)!.universal_id}
floors={data.floors}
mapStyle={`${getUrl(URLTypes.STYLES)}/styles.json`}
accessToken={process.env.REACT_APP_MAPBOX_TOKEN as string}
onMapReady={(map) => {
mapInstance.current = map;
userLocationControl.current = map.getPluginById<UserLocationControl>(
PLUGIN_IDS.USER_LOCATION
);
routingControl.current = map.getPluginById<RoutingControl>(
PLUGIN_IDS.ROUTING
);
geofenceControl.current = map.getPluginById<GeofenceControl>(
PLUGIN_IDS.GEOFENCE
);
floorControl.current = map.getPluginById<FloorControl>(
PLUGIN_IDS.FLOOR
);
}}
/>
</div>
) : isError ? (
<div>Error loading config</div>
) : null;
}
Is there a way by which I can keep a single instance of a mapbox map while also reading/writing data to the redux store and interacting with the map instance without causing the component to re-mount the map component each time?
It appears it was re-rendering needlessly as I was passing a function into the map component without wrapping it in useCallback()

setState doesn't re-render react functional component

I am getting user's location and setting it as the new state at "onSuccess" function,
the component doesn't re-render.
After checking a lot i have seen that react doesn't see it as a change of state because it is an array in that case and it doesn't pass react's "equality" check as a state that was changed.
With that, nothing that i have tried has worked. any ideas?
import { useEffect, useState } from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
export default function Map() {
const [location, setLocation] = useState([52.234, 13.413]);
const onSuccess = (position) => {
let userLocation = [position.coords.latitude, position.coords.longitude];
setLocation([...userLocation]);
};
useEffect(() => {
if (!("geolocation" in navigator)) {
alert("no Geolocation available");
}
navigator.geolocation.getCurrentPosition(onSuccess);
}, []);
console.log(location);
return (
<>
<MapContainer
className="leaflet-map"
center={location}
zoom={11}
scrollWheelZoom={false}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[51.505, -0.09]}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>
</>
);
}
It looks like MapContainer does not recenter after mounting even if the center prop is changed as mentioned in the docs:
https://react-leaflet.js.org/docs/api-map
Except for its children, MapContainer props are immutable: changing them after they have been set a first time will have no effect on the Map instance or its container.
You could force replacing the MapContainer by passing a key prop that you change whenever location changes, for example:
<MapContainer
key={`${location[0]}-${location[1]}`}
Or investigating other options in react-leaflet such as useMap to get access to the Leaflet Map instance and calling map.setView(...) https://leafletjs.com/reference-1.7.1.html#map-setview
Are you able to confirm that onSuccess is called at all? It may be that getCurrentPosition is running into an error, so calling it with two arguments would be good:
navigator.geolocation.getCurrentPosition(onSuccess, onError);
You should also include onSuccess in the useEffect dependencies.
useEffect(() => {
if (!("geolocation" in navigator)) {
alert("no Geolocation available");
}
navigator.geolocation.getCurrentPosition(onSuccess);
}, [onSuccess]);
And to prevent multiple calls to getCurrentPosition due to onSuccess changing, you should also useCallback with the dependency on setLocation:
const onSuccess = useCallback((position) => {
let userLocation = [position.coords.latitude, position.coords.longitude];
setLocation([...userLocation]);
}, [setLocation]);

"this.props" not working in my on mouseup event function in Map Component using react and mapbox-gl

Hello I'm pretty new to coding and I'm having an issue that I cannot resolve. I have a MapBox map that loads up with incident icons per lat/lon and radius. Using react I created a Map component and in the map component I added a mouseup event that grabs the lat and lot when the user clicks on the map and I want to take those values and feed them into the loadIncidentsAndRoads function which fetches the incidents api call. My problem is that this.props.incidentsActions.loadIncidentsAndRoads(metroId, tmcMetro, setCoords) is not valid within the mouseup event yet if you execute the function outside of the mouseup event it works. Im not really sure what is needed to help any answer so if you need more info please let me know and I will provide them.
<Map
center={center}
zoom={zoom}
minZoom={4}
maxZoom={20}
style={''}
onStyleLoad={map => {
map.addSource('trafficTiles', trafficTiles);
window.map = map;
window.addEventListener('resize', this._resizeMap.bind(null, map));
map.addControl(new mapboxgl.FullscreenControl({container: document.querySelector('body')}), "top-right");
map.on('mouseup', function (e) {
const getCoords = e.lngLat.wrap();
console.log(getCoords);
const setCoords = [getCoords.lng, getCoords.lat];
console.log(setCoords);
const metroId = 140;
const tmcMetro = 45;
return(
//this load Incident function does not work because of the this.props is not valid
this.props.incidentActions.loadIncidentsAndRoads(metroId, tmcMetro, setCoords)
);
});
const metroId = 140;
const tmcMetro = 45;
// this load Incident function works but just with hard coded values.
this.props.incidentActions.loadIncidentsAndRoads(metroId, tmcMetro, setCoords);
}}
onDrag={this._onDrag}
onMoveEnd={this._setMove.bind(this, true)}
onMove={this._setMove.bind(this, false)}
containerStyle={mapWrapper}>
<ZoomControl
style={{margin: '95px 10px'}}
zoomDiff={1}
onControlClick={this._onControlClick}/>
<Source
id="source_id"
tileJsonSource={trafficTiles}/>
<Layer
id="trafficTiles"
sourceId="source_id"
type="raster"
layerOptions={{
"source-layer": "trafficTiles"
}}
layout={{}}
paint={{"raster-opacity": 1.0}}/>
The meaning of this is not preserved once you get into a function. You can try using an arrow function, which preserves the meaning of this:
map.on('mouseup', (e) => {
const getCoords = e.lngLat.wrap();
const setCoords = [getCoords.lng, getCoords.lat];
const metroId = 140;
const tmcMetro = 45;
return(
this.props.incidentActions.loadIncidentsAndRoads(metroId, tmcMetro, setCoords)
);
});
If that is still problematic, that means that the map.on function is not preserving the meaning of this, and you'd have to pass it another way. Earlier in your component, before your return(...), you can destructure incidentActions from your props, and use it directly:
// before return( ... ) of the component
const { incidentActions } = this.props
//back within your <Map>
map.on('mouseup', function (e) {
const getCoords = e.lngLat.wrap();
const setCoords = [getCoords.lng, getCoords.lat];
const metroId = 140;
const tmcMetro = 45;
return(
incidentActions.loadIncidentsAndRoads(metroId, tmcMetro, setCoords)
);
});

useState not setting after initial setting

I have a functional component that is using useState and uses the #react-google-maps/api component. I have a map that uses an onLoad function to initalize a custom control on the map (with a click event). I then set state within this click event. It works the first time, but every time after that doesn't toggle the value.
Function component:
import React, { useCallback } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const [radiusDrawMode, setRadiusDrawMode] = React.useState(false);
const toggleRadiusDrawMode = useCallback((map) => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
}, [setRadiusDrawMode, radiusDrawMode]); // Tried different dependencies.. nothing worked
const mapInit = useCallback((map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
}, [toggleRadiusDrawMode, radiusDrawMode, setRadiusDrawMode]); // Tried different dependencies.. nothing worked
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
The first time the user presses the button on the map, it setss the radiusDrawMode to true and sets the correct cursor for the map (crosshair). Every click of the button after does not update radiusDrawMode and it stays in the true state.
I appreciate any help.
My guess is that it's a cache issue with useCallback. Try removing the useCallbacks to test without that optimization. If it works, you'll know for sure, and then you can double check what should be memoized and what maybe should not be.
I'd start by removing the one from toggleRadiusDrawMode:
const toggleRadiusDrawMode = map => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
};
Also, can you access the state of the map options (the ones that you're setting with map.setOptions)? If so, it might be worth using the actual state of the map's option rather than creating your own internal state to track the same thing. Something like (I'm not positive that it would be map.options):
const toggleRadiusDrawMode = map => {
const { draggableCursor } = map.options;
map.setOptions({
draggableCursor: draggableCursor === 'grab' ? 'crosshair' : 'grab'
});
};
Also, I doubt this is the issue, but it looks like you're missing a closing bracket on the <GoogleMap> element? (Also, you might not need to create the intermediary function between onLoad and mapInit, and can probably pass mapInit directly to the onLoad.)
<GoogleMap id='google-map'
onLoad={mapInit}>
This is the solution I ended up using to solve this problem.
I basically had to switch out using a useState(false) for setRef(false). Then set up a useEffect to listen to changes on the ref, and in the actual toggleRadiusDraw I set the reference value which fires the useEffect to set the actual ref value.
import React, { useCallback, useRef } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const radiusDrawMode = useRef(false);
let currentRadiusDrawMode = radiusDrawMode.current;
useEffect(() => {
radiusDrawMode.current = !radiusDrawMode;
});
const toggleRadiusDrawMode = (map) => {
map.setOptions({ draggableCursor: (!currentRadiusDrawMode) ? 'crosshair' : 'grab' });
currentRadiusDrawMode = !currentRadiusDrawMode;
};
const mapInit = (map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
});
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
Not sure if this is the best way to handle this, but hope it helps someone else in the future.

Resources