这是我的状态:
const [markers, setMarkers] = useState([])
我在useEffect挂钩中初始化Leaflet映射。它具有一个click
eventHandler。
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
。始终是初始状态。我试图通过按钮的onClick处理程序调用{{1}}。在那里我可以阅读onMapClick
。如果原始事件始于地图,为什么我看不到markers
?如何读取markers
内部的状态变量?
这里是一个示例:https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js
当您单击地图并查看控制台时,您会发现onMapClick
中的markers
数组在填充onMapClick
来监听useEffect
的过程中保持为空
答案 0 :(得分:1)
反应状态是异步的,它不能立即保证您为您提供新状态,关于您的问题如果原始事件始于地图,为什么我不能读取标记异步性质以及状态值由函数根据其当前关闭和状态更新而使用的事实将反映在下一次重新渲染中,通过该重新渲染,现有的关闭不会受到影响,但是会创建新的关闭,您将不会在类组件上遇到此问题因为其中有此实例,并且具有全局范围。
在开发组件时,我们应确保从调用位置控制组件,而不是处理状态的函数闭包,它会在每次状态更改时重新呈现。您的解决方案是可行的,无论何时需要将传递给函数的事件或操作传递给它,都应传递一个值。
编辑:-它只是简单地将参数或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/,这将对您有长期帮助!
答案 1 :(得分:1)
很长,但你会明白为什么会发生这种情况以及更好的修复。闭包尤其是一个问题(也很难理解),主要是当我们设置依赖于状态的点击处理程序时,如果具有新作用域的处理程序函数没有重新附加到点击事件,然后闭包保持未更新,因此陈旧状态保留在点击处理函数中。
如果您在组件中完全理解它,useCallback
将返回对更新函数的新引用,即 onMapClick
在其范围内具有您更新的标记(状态) ,但由于您仅在安装组件时才在开始时设置“点击”处理程序,因此点击处理程序保持未更新,因为您已选中 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 不是用新函数替换点击处理程序,而是 adds another event handler(基本上是堆叠事件处理程序),所以我们必须在添加新的之前删除旧的,否则每次 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
块的版本,因为它易于修改并且您可以了解其背后的逻辑。