如何在事件处理程序中访问React状态?

3
这是我的状态:
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。它总是初始状态。我尝试通过单击处理程序的onClick调用onMapClick。在那里,我可以读取markers。为什么如果原始事件在地图上开始,我不能读取markers?如何在onMapClick内读取状态变量?

以下是示例:https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js 当您单击地图并查看控制台时,您会发现onMapClick中的markers数组保持为空,而在侦听markersuseEffect中填充。

2个回答

3
长篇大论,但你会明白为什么会发生这种情况以及更好的解决方法。闭包是一个特别的问题(也很难理解),主要是当我们设置依赖于状态的点击处理程序时,如果新范围的处理函数没有重新附加到点击事件上,则闭包保持未更新,因此旧状态仍然存在于点击处理程序函数中。
如果您在组件中完全理解它,useCallback将返回对已更新函数的新引用,即onMapClick在其范围内具有更新后的标记(状态),但由于您仅在组件挂载时设置了“click”处理程序,因此该处理程序保持未更新,因为您放置了一个检查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]);

这是更新后的链接sandbox,目前运行正常。
现在有另一个解决方法,可以避免每次更换点击处理程序。即一些全局变量,我相信这并不是太糟糕。
为此,在组件之外但上面添加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

这个也完美地工作了,使用这段代码链接到sandbox。它运行得更快。
最后,将其作为参数传递的解决方案也可以。我更喜欢带有更新的if块的解决方案,因为它易于修改并且您可以理解其中的逻辑。

1
React状态是异步的,并不能立即保证给你新的状态。至于你的问题“为什么如果原始事件在地图上开始,我就无法读取标记”,这是一种异步的本质,并且基于它们当前闭包的状态值被函数使用,状态更新将反映在下一个重新渲染中,而现有的闭包不受影响,但会创建新的闭包。对于类组件,你不会遇到这个问题,因为它具有全局范围的实例this
作为开发组件,我们应该确保从你调用它的位置控制组件,而不是函数闭包处理状态,每次状态更改都会重新呈现。你的解决方案是可行的,你应该在需要时传递一个事件或操作的值。
编辑:-很简单,只需将参数或依赖项传递给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/,这将有助于您长期发展!!!


谢谢你的回答。我知道React状态是异步的,但我不太清楚如何重构我的解决方案,使得地图事件调用的事件处理程序可以访问状态。 - Juuro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接