如何在我的 eventHandler 中访问 React 状态?

IT技术 reactjs leaflet react-hooks react-leaflet use-state
2021-05-24 03:47:22

这是我的状态:

const [markers, setMarkers] = useState([])

我在 useEffect 钩子中初始化了一个 Leaflet 地图。它有一个事件click处理器。

useEffect(() => {
    map.current = Leaflet.map('mapid').setView([46.378333, 13.836667], 12)
    .
    .
    .
    map.current.on('click', onMapClick)
}, []

在里面,onMapClick我在地图上创建了一个标记并将其添加到状态中:

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])

但我也想访问markers这里状态。但我不能markers在这里阅读它始终是初始状态。我试图onMapClick通过按钮的 onClick 处理程序调用在那里我可以阅读markersmarkers如果原始事件从地图开始,为什么我无法阅读如何读取里面的状态变量onMapClick

这是一个示例:https : //codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js 当您单击地图并查看控制台时,您会看到markers数组onMapClick保持为空当它被填入useEffect监听markers.

2个回答

React 状态是异步的,它不会立即保证您为您提供新状态,至于您的问题,如果原始事件从地图开始,为什么我不能读取标记,其异步性质以及使用状态值的事实by 函数基于它们当前的闭包和状态更新将反映在下一次重新渲染中,现有的闭包不会受到影响但会创建新的闭包,这个问题你不会在类组件上遇到,因为你有这个实例,它有全球范围。

作为开发组件,我们应该确保从调用它的地方控制组件,而不是处理状态的函数闭包,它会在每次状态更改时重新渲染。您的解决方案是可行的,您应该在需要时传递一个值,无论您传递给函数的事件或操作是什么。

编辑:-它的简单只是将 params 或 deps 传递给 useEffect 并将您的回调包装在里面,对于您的情况,它将是

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

有关更多信息,请查看https://dmitripavlutin.com/react-hooks-stale-closures/ ,它将为您提供更长期的帮助!!!

很长,但你会明白为什么会发生这种情况以及更好的修复。闭包尤其是一个问题(也很难理解),主要是当我们设置依赖于 state 的点击处理程序,如果具有新作用域的处理程序函数没有重新附加到点击事件,那么闭包将保持未更新和因此,陈旧状态保留在点击处理函数中

如果您在组件中完全理解它,useCallback则会返回对更新函数的新引用,即在其范围内onMapClick 具有更新的标记(状态),但是因为您仅在安装组件时才在开始时设置“单击”处理程序,点击处理程序保持未更新,因为您已经放置了一个 check if(! map.current),这会阻止任何新的处理程序附加到地图上。

// 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]);

现在我尝试map.current.on("click", onMapClick);移出if块,但是有一个问题,Leaflets 不是用新函数替换点击处理程序,而是添加了另一个事件处理程序(基本上是堆叠事件处理程序),所以我们必须在添加新的之前删除旧的,否则我们最终会在每次onMapClick更新时添加多个处理程序我们有off()函数。

这是更新后的代码

// 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]);

这是更新后的沙箱的链接,它运行良好。

现在有另一个想法来解决它,而无需每次都更换点击处理程序。即一些全局变量,我相信这还不算太糟糕。

为此globalMarkers,在组件之外但在组件上方添加并每次更新它。

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

这个也很完美,用这个代码链接到沙箱这个工作得更快。

最后,上面将它作为参数传递的解决方案也可以!我更喜欢带有更新if块的那个,因为它很容易修改,而且你可以理解它背后的逻辑。