问题
This is my state:
const [markers, setMarkers] = useState([])
I initialise a Leaflet map in a useEffect hook. It has a click
eventHandler.
useEffect(() => {
map.current = Leaflet.map('mapid').setView([46.378333, 13.836667], 12)
.
.
.
map.current.on('click', onMapClick)
}, []
Inside that onMapClick
I create a marker on the map and add it to the state:
const onMapClick = useCallback((event) => {
console.log('onMapClick markers', markers)
const marker = Leaflet.marker(event.latlng, {
draggable: true,
icon: Leaflet.divIcon({
html: markers.length + 1,
className: 'marker-text',
}),
}).addTo(map.current).on('move', onMarkerMove)
setMarkers((existingMarkers) => [ ...existingMarkers, marker])
}, [markers, onMarkerMove])
But I would also like to access the markers
state here. But I can't read markers
here. It's always the initial state. I tried to call onMapClick
via a onClick handler of a button. There I can read markers
. Why can't I read markers
if the original event starts at the map? How can I read the state variables inside onMapClick
?
Here is an example: https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js
When you click in the map and have a look at the console you see that the markers
array in onMapClick
stays empty while it gets filled in the useEffect
that listens for markers
.
回答1:
React state is asynchronous and it won't immediately guarantee you to give you the new state, as for your question Why can't I read markers if the original event starts at the map its an asynchronous nature and the fact that state values are used by functions based on their current closures and state updates will reflect in the next re-render by which the existing closures are not affected but new ones are created, this problem you wont face on class components as you have this instance in it, which has global scope.
As a developing a component , we should make sure the components are controlled from where you are invoking it, instead of function closures dealing with state , it will re-render every time state changes . Your solution is viable you should pass a value whatever event or action you pass to a function, when its required.
Edit:- its Simple just pass params or deps to useEffect and wrap your callback inside, for your case it would be
useEffect(() => {
map.current = Leaflet.map('mapid').setView([46.378333, 13.836667], 12)
.
.
.
map.current.on('click',()=> onMapClick(markers)) //pass latest change
}, [markers] // when your state changes it will call this again
for more info check this one out https://dmitripavlutin.com/react-hooks-stale-closures/ , it will help you for longer term !!!
回答2:
Long one but you'll understand why this is happening and the better fixes. Closures are especially an issue (also hard to understand), mostly when we set click handlers which are dependent on the state, if the handler function with the new scope is not re-attached to the click event, then closures remain un-updated and hence the stale state remains in the click handler function.
If you understand it perfectly in your component, useCallback
is returning a new reference to the updated function i.e onMapClick
having your updated markers ( the state) in its scope, but since you are setting the 'click' handler only in the beginning when the component is mounted, the click handler remains un-updated since you've put a check if(! map.current)
, which prevents any new handler to be attached on the map.
// in sandbox map.js line 40
useEffect(() => {
// this is the issue, only true when component is initialized
if (! map.current) {
map.current = Leaflet.map("mapid4").setView([46.378333, 13.836667], 12);
Leaflet.tileLayer({ ....}).addTo(map.current);
// we must update this since onMapClick was updated
// but you're preventing this from happening using the if statement
map.current.on("click", onMapClick);
}
}, [onMapClick]);
Now I tried moving map.current.on("click", onMapClick);
out of the if
block, but there's an issue, Leaflets instead of replacing the click handler with the new function, it adds another event handler ( basically stacking event handlers ), so we must remove the old one before adding the new one, otherwise we will end up adding multiple handlers each time onMapClick
is updated. For which we have the off()
function.
Here's the updated code
// in sandbox map.js line 40
useEffect(() => {
// this is the issue, only true when component is initialized
if (!map.current) {
map.current = Leaflet.map("mapid4").setView([46.378333, 13.836667], 12);
Leaflet.tileLayer({ ....
}).addTo(map.current);
}
// remove out of the condition block
// remove any stale click handlers and add the updated onMapClick handler
map.current.off('click').on("click", onMapClick);
}, [onMapClick]);
This is the link to the updated sandbox which is working just fine.
Now there's another Idea to solve it without replacing click handler each time. i.e some globals, which I believe is not really too bad.
For this add globalMarkers
outside but above your component and update it each time.
let updatedMarkers = [];
const Map4 = () => {
let map = useRef(null);
let path = useRef({});
updatedMarkers = markers; // update this variable each and every time with the new markers value
......
const onMapClick = useCallback((event) => {
console.log('onMapClick markers', markers)
const marker = Leaflet.marker(event.latlng, {
draggable: true,
icon: Leaflet.divIcon({
// use updatedMarkers here
html: updatedMarkers.length + 1,
className: 'marker-text',
}),
}).addTo(map.current).on('move', onMarkerMove)
setMarkers((existingMarkers) => [ ...existingMarkers, marker])
}, [markers, onMarkerMove])
.....
} // component end
And this one works perfectly too, Link to the sandbox with this code. This one works faster.
And lastly, the above solution of passing it as a param is okay too! I prefer the one with updated if
block since it's easy to modify and you get the logic behind it.
来源:https://stackoverflow.com/questions/63307310/how-can-i-access-react-state-in-my-eventhandler