How can I access React state in my eventHandler?

久未见 提交于 2021-01-24 07:22:44

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!