How to make dynamic changes in Arcgis JS map-component with React? - reactjs

I am quite new to Arcgis-JS and React. As suggested here, I am using a functional component with the useEffect hook to integrate my map.
Now I want to display a line within my map when I click on a certain row of a list. On click I am fetching the appropriate coordinates to be displayed and storing them to a context-variable (dbPageContext.currentGeom).
The problem: When I want to display another line, the entire map-component has to re-render as I am passing the line-array-variable as a second argument to the useEffect hook.
const MapComp = () => {
const mapRef = useRef();
const dbPageContext = useContext(DbPageContext);
useEffect(() => {
const mainMap = new Map({
layers: [layer],
basemap: "arcgis-topographic", // Basemap layer service
});
const graphicsLayer = new GraphicsLayer();
mainMap.add(graphicsLayer);
const polyline = {
type: "polyline",
paths: dbPageContext.currentGeom,
};
const simpleLineSymbol = {
type: "simple-line",
color: [0, 230, 250],
width: 4,
};
const polylineGraphic = new Graphic({
geometry: polyline,
symbol: simpleLineSymbol,
});
graphicsLayer.add(polylineGraphic);
const view = new MapView({
container: mapRef.current,
map: mainMap,
spatialReference: {
wkid: 3857,
},
});
return () => {
view && view.destroy();
};
}, [dbPageContext.currentGeom]);
return (
<div>
<div className="webmap" ref={mapRef} />
</div>
);
};
export default MapComp;
How can I update only the graphics-layer without updating the entire map-component? Would be great if someone could help me finding a solution for that.
EDIT: I also tried to implement the map without using the useeffect hook. But then, nothing was displayed.

You need to separate the effects. On page load, you should have one effect that creates the map. Then a second effect can update the map when dbPageContext.currentGeom changes.
const MapComp = () => {
const mapRef = useRef();
const dbPageContext = useContext(DbPageContext);
// Memoize this, as you only need to create it once, but you also need
// it to be available within scope of both of the following useEffects
const graphicsLayer = React.useMemo(
() => new GraphicsLayer(),
[]
);
// On mount, create the map, view, and teardown
useEffect(() => {
const mainMap = new Map({
layers: [layer],
basemap: "arcgis-topographic", // Basemap layer service
});
const view = new MapView({
container: mapRef.current,
map: mainMap,
spatialReference: {
wkid: 3857,
},
});
mainMap.add(graphicsLayer);
return () => {
view && view.destroy();
};
}, [])
// When dbPageContext.currentGeom changes, add a polyline
// to the graphics layer
useEffect(() => {
const polyline = {
type: "polyline",
paths: dbPageContext.currentGeom,
};
const simpleLineSymbol = {
type: "simple-line",
color: [0, 230, 250],
width: 4,
};
const polylineGraphic = new Graphic({
geometry: polyline,
symbol: simpleLineSymbol,
});
// Clear previously added lines (if that's what you want)
graphicsLayer.removeAll()
graphicsLayer.add(polylineGraphic);
}, [dbPageContext.currentGeom]);
return (
<div>
<div className="webmap" ref={mapRef} />
</div>
);
};
export default MapComp;

Related

How to pass ref to draw control in react-map-gl

Following the Mapbox draw example I can use the draw variable to access all features that are drawn on a map.
const draw = new MapboxDraw({
// ...
});
map.addControl(draw);
// ...
function updateArea(e) {
const data = draw.getAll(); // Accessing all features (data) drawn here
// ...
}
However, in react-map-gl library useControl example I can not figure out how to pass ref to the DrawControl component so I can use it as something like draw.current in a similar way as I did draw in normal javascript above.
In my DrawControl.jsx
const DrawControl = (props) => {
useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
In my MapDrawer.jsx
import Map from 'react-map-gl';
import DrawControl from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = React.useRef(null);
const onUpdate = React.useCallback((e) => {
const data = draw.current.getAll(); // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
ref={draw}
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
I also get an error stating I should use forwardRef but I'm not really sure how.
react_devtools_backend.js:3973 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
What I need is basically to delete the previous feature if there is a new polygon drawn on a map so that only one polygon is allowed on a map. I want to be able to do something like this in the onUpdate callback.
const onUpdate = React.useCallback((e) => {
// ...
draw.current.delete(draw.current.getAll.features[0].id);
// ...
}, []);
I had the similar problem recently with that lib, I solved it doing the following :
export let drawRef = null;
export default const DrawControl = (props) => {
drawRef = useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
import DrawControl, {drawRef} from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = drawRef;
const onUpdate = React.useCallback((e) => {
const data = draw?draw.current.getAll():null; // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
const onUpdate = React.useCallback((e) => {
// ...
drawRef.delete(drawRef.getAll.features[0].id);
// ...
}, []);
Once component created, the ref is available for use.
Not that elegant but working... Sure there might be cleaner way...
Hope that helps!
Cheers
Pass draw from draw control to parent component.
const DrawControl = (props) => {
const [draw, setDraw] = useState()
const { setDraw: setDrawInParent, onUpdate, onCreate, onDelete } = props;
useEffect(() => {
if (draw) setDrawInParent(draw)
}, [draw])
useControl(
({ map }) => {
map.on("draw.create", onCreate);
map.on("draw.update", onUpdate);
map.on("draw.delete", onDelete);
const draw = new MapboxDraw(props);
setDraw(draw);
return draw;
}
);
return null;
};
I think I found a better solution combine forwardRef and useImperativeHandle to solve:
export const DrawControl = React.forwardRef((props: DrawControlProps, ref) => {
const drawRef = useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ map }) => {
map.on("draw.create", props.onCreate);
map.on("draw.update", props.onUpdate);
map.on("draw.delete", props.onDelete);
map.on("draw.modechange", props.onModeChange);
},
({ map }) => {
map.off("draw.create", props.onCreate);
map.off("draw.update", props.onUpdate);
map.off("draw.delete", props.onDelete);
map.off("draw.modechange", props.onModeChange);
},
{
position: props.position,
}
);
React.useImperativeHandle(ref, () => drawRef, [drawRef]); // This way I exposed drawRef outside the component
return null;
});
in the component:
const drawRef = React.useRef<MapboxDraw>();
const [drawMode, setDrawMode] = React.useState<DrawMode>(“draw_polygon");
const changeModeTo = (mode: DrawMode) => {
// If you programmatically invoke a function in the Draw API, any event that directly corresponds with that function will not be fired
drawRef.current?.changeMode(mode as string);
setDrawMode(mode);
};
<>
<DrawControl
ref={drawRef}
position="top-right”
displayControlsDefault={false}
controls={{
polygon: true,
trash: true,
}}
defaultMode=“draw_polygon"
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onModeChange={onModeChange}
/>
<button
style={{
position: ‘absolute’,
left: ‘20px’,
top: ‘20px’,
backgroundColor: '#ff0000’,
}}
onClick={() => changeModeTo('simple_select’)}
>
Change to Simple Select
</button>
<>

Assigning ref in react-konva

How do we assign ref to the stage or layer object. I am using react-konva. When I do console.log(stageE1), it says undefined.
useEffect(() => {
var stage = new Konva.Stage({
container: 'stage',
width: window.innerWidth,
height: window.innerHeight,
ref: stageEl, // not working
onMouseDown: (e) => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage()
if (clickedOnEmpty) {
selectShape(null)
}
},
})
stage.container().style.border = '1px solid grey'
var layer = new Konva.Layer({
ref: layerEl, // not working
})
stage.add(layer)
}, [])
This is not react-konva usage. You are using Konva API directly. If you do this, you probably don't need to use refs. But if you really want:
var layer = new Konva.Layer();
// set ref
layerEl.current = layer;
If you use react-konva, then you should define components in React way:
import { Stage, Layer } from 'react-konva';
const App = () => {
const layerEl = React.useRef();
return <Stage><Layer ref={layerEl} /></Stage>;
};

Chart refreshing twice

Chart created using react-chartjs-2 and chartjs is refreshing 2 times on changing values. For the first time, only the shape of the graph is changing and for the second time, the values on the y-axis and x-axis are changing.
const Chart = ({country}) => {
const [dailyData,setDailyData] = useState([]);
useEffect(() => {
const fetchAPI = async () => {
setDailyData(await fetchDailyData(country))
}
fetchAPI();
},[country]);
const {day,infected,recoveries,deaths} = dailyData;
const lineChart = (
infected? (<Line
data={{
labels: day.map((data ) => (data)),
datasets:[
{
data: infected.map((data) => (data)),
label: 'Infected',
borderColor: '#3333ff',
fill: true,
}],
}}
/> ) : null
)
There is an issue with your dailyData state.
You set the default value as an empty array but then later you try to destructure it as an object.
const asd=[1,2,4,5];
const {a,b,c}=asd;
console.log (a,b,c); // undefined, undefined, undefined
const [e,f,g]=asd;
console.log (e,f,g); // 1,2,4
However, you said your chart does work after the second flash. Meaning your original empty array is not the right default value for the useState hook. Should use null and check this before you destructure it.
UPDATE:
The chart could flash twice if the data is loaded twice too. useEffect should have an if statement checking if the data was loaded already and load only if it was not. Possibly you could add another state for loading. so you would not have two concurrent API calls.
Your code would look like this:
const Chart = ({country}) => {
const [dailyData,setDailyData] = useState(null);
useEffect(() => {
if(country && !dailyData){
const fetchAPI = async () => {
setDailyData(await fetchDailyData(country))
}
fetchAPI();
}
},[country]);
let lineChart=null
if (dailyData){
const {day,infected,recoveries,deaths} = dailyData;
lineChart = (<Line
data={{
labels: day.map((data ) => (data)),
datasets:[
{
data: infected.map((data) => (data)),
label: 'Infected',
borderColor: '#3333ff',
fill: true,
}],
}}
/> )
}
}

How to query routes in here maps with reactjs

I am trying to make map queries with here maps on routes with reactjs, currently I can display the map in my application with this code:
import * as React from 'react';
export const DisplayMapFC = () => {
// Create a reference to the HTML element we want to put the map on
const mapRef = React.useRef(null);
/**
* Create the map instance
* While `useEffect` could also be used here, `useLayoutEffect` will render
* the map sooner
*/
React.useLayoutEffect(() => {
// `mapRef.current` will be `undefined` when this hook first runs; edge case that
if (!mapRef.current) return;
const H = window.H;
const platform = new H.service.Platform({
apikey: "xxxxxxxxx"
});
const defaultLayers = platform.createDefaultLayers();
const hMap = new H.Map(mapRef.current, defaultLayers.vector.normal.map, {
center: { lat: 36.2104063, lng: -113.7236071 }, //,
zoom: 4,
pixelRatio: window.devicePixelRatio || 1
});
const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(hMap));
const ui = H.ui.UI.createDefault(hMap, defaultLayers);
// This will act as a cleanup to run once this hook runs again.
// This includes when the component un-mounts
return () => {
hMap.dispose();
};
}, [mapRef]); // This will run this hook every time this ref is updated
return <div className="map" ref={mapRef} style={{ height: "355px",width:"650px" }} />;
};
const searchMap=(e)=>{
e.preventDefault();
console.log(" view controle ",e);
const origin="52.5308,13.3847";
const destiny="52.5264,13.3686";
}
I am trying to send coordinates from a start point to an end point so that I can show the plotted route and time
this worked for me:
Change API_KEY, origin and destination. Luck!
https://codesandbox.io/s/react-with-heremap-2cze0?file=/src/mapHere.js

Click handler on mapboxGL not respondig to updated state using react hooks with typescript

I am working on this simple hiking application, with display a map in a modal where i want to set a start point and end point in the modal components state, and then update the parents state.
The problem is that the click handler in the modal does not "see" the updated state.
If i console log the state outside of the click handler, it gives me the updated state.
Im using mapboxGL js, and i wonder if someone knows why this is happening? I am thinking maybe it as something to do with the 'click' event, since it not react onClick event?
Here is the code for the modal component:
export const MapModalContent = ({
newHike, setNewHike, setShowMapModal,
}: MapModalProps) => {
const [map, setMap] = useState<mapboxgl.Map>();
const [startCoords, setStartCoords] = useState<LngLat>();
const [endCoords, setEndCoords] = useState<LngLat>();
const mapRef: React.MutableRefObject<HTMLDivElement | null> = useRef(null);
const [helperString, setHelperString] = useState<IHelperString>(helperStrings[0]);
const [startMarker] = useState(new mapboxgl.Marker());
const [endMarker] = useState(new mapboxgl.Marker());
const [startPointIsSet, setStartPointIsSet] = useState<boolean>(false);
const [endPointIsSet, setEndPointIsSet] = useState<boolean>(false);
// initializes map
useEffect(() => {
if (mapRef.current && !map) {
setMap(new mapboxgl.Map({
accessToken: MAPBOX_ACCESS_TOKEN,
container: mapRef.current,
style: 'mapbox://styles/mapbox/outdoors-v11',
center: [10.748503539483494, 59.92003719905571],
zoom: 10,
}));
}
}, [mapRef]);
// adds controls and click listener to map
useEffect(() => {
if (map) {
addControls({ to: map });
map.on('click', (e) => {
handleSetMarker(e);
});
}
}, [map]);
// these effects console the updated state as wanted
useEffect(() => {
console.log('Start coord: ', startCoords);
}, [startCoords]);
useEffect(() => {
console.log('End coord: ', endCoords);
}, [endCoords]);
useEffect(() => {
console.log('Start is set: ', startPointIsSet);
}, [startPointIsSet]);
useEffect(() => {
console.log('End is set: ', endPointIsSet);
}, [endPointIsSet]);
// Todo: Remove this..
setTimeout(() => {
if (map) map.resize();
}, 500);
// this is the click handler that does not respond to state changes
const handleSetMarker = (e: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {
console.log('👆', `start point: ${startPointIsSet}`, `end point: ${endPointIsSet}`);
if (!startPointIsSet) {
console.log('Start point not set.. Gonna set it now!');
// setStartCoords(e.lngLat);
startMarker.setLngLat(e.lngLat).addTo(map!);
setStartCoords(e.lngLat);
setStartPointIsSet(true);
}
if (startPointIsSet && !endPointIsSet) {
console.log('Start point is set! Setting end point..');
endMarker.setLngLat(e.lngLat).addTo(map!);
setEndCoords(e.lngLat);
setEndPointIsSet(true);
}
};
const handleButtonTap = () => {
if (startCoords) {
// displays a new message to the user after setting start point
setHelperString(helperStrings[1]);
} else {
console.warn('No start coords set!');
}
// sets parents state
if (startCoords && endCoords) {
setNewHike({
...newHike,
start_point: getPointString({ from: startCoords }),
end_point: getPointString({ from: endCoords }),
});
setShowMapModal(false);
} else {
console.warn('Some coords is null!', startCoords, endCoords);
}
};
return (
<>
<MapContainer ref={mapRef} />
<p style={{ margin: '1em auto 1em auto' }}>{ helperString.sentence }</p>
<IonButton onClick={handleButtonTap}>{ helperString.button }</IonButton>
</>
);
};
I've tried lifting the state up to the parent, but it gave me the exact same result.
I've also tried adding two separate click events to the map with no luck.
And I gave it a try with the 'react-mapbox-gl' wrapper, but the same problem arose.
It looks like because your handler is attached as a callback, it closes over the React state.
map.on('click', (e) => {
handleSetMarker(e);
});
Try useCallback with the proper dependencies.
const handleSetMarker = useCallback((e: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {
console.log('👆', `start point: ${startPointIsSet}`, `end point: ${endPointIsSet}`);
if (!startPointIsSet) {
console.log('Start point not set.. Gonna set it now!');
// setStartCoords(e.lngLat);
startMarker.setLngLat(e.lngLat).addTo(map!);
setStartCoords(e.lngLat);
setStartPointIsSet(true);
}
if (startPointIsSet && !endPointIsSet) {
console.log('Start point is set! Setting end point..');
endMarker.setLngLat(e.lngLat).addTo(map!);
setEndCoords(e.lngLat);
setEndPointIsSet(true);
}
}, [startPointIsSet, endPointIsSet, endMarker, startMarker, map]);

Resources