如何在我的事件处理程序中访问 React 状态?
How can I access React state in my eventHandler?
这是我的状态:
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
。它始终是初始状态。我试图通过按钮的 onClick 处理程序调用 onMapClick
。在那里我可以阅读 markers
。如果原始事件在地图上开始,为什么我无法读取 markers
?如何读取 onMapClick
中的状态变量?
这是一个例子:https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js
当您在地图中单击并查看控制台时,您会看到 onMapClick
中的 markers
数组在侦听 markers
的 useEffect
中填充时保持为空.
React 状态是异步的,它不会立即保证给你新的状态,至于你的问题如果原始事件从地图开始,为什么我不能读取标记 它是一种异步性质,并且函数基于它们当前的闭包使用状态值,状态更新将反映在下一个 re-render 中,现有闭包不会受到影响,但会创建新的闭包,这你不会在 class 组件上遇到的问题,因为你有 this 实例,它具有全局范围。
作为开发组件,我们应该确保组件是从您调用它的地方控制的,而不是处理状态的函数闭包,它会在每次状态更改时 re-render。您的解决方案是可行的,您应该在需要时将值传递给您传递给函数的任何事件或操作。
编辑:- 很简单,只需将参数或 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/ ,它将对您提供更长期的帮助 !!!
很长,但您会理解为什么会发生这种情况以及更好的修复方法。闭包尤其是一个问题(也很难理解),主要是当我们设置 依赖于状态 的点击处理程序时,如果具有新作用域的处理程序函数没有重新附加到单击事件,然后闭包保持未更新,因此 陈旧状态保留在单击处理程序函数中。
如果您在组件中完全理解它,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 没有用新函数替换点击处理程序,它 adds another event handler (基本上堆叠event handlers ),因此我们必须在添加新的之前删除旧的,否则每次更新 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 的 link,工作正常。
现在有另一个想法可以解决它,而无需每次都替换点击处理程序。即一些全局变量,我认为这并不是太糟糕。
为此,在您的组件外部但上方添加 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
这个也可以完美地工作,Link 到 sandbox 使用此代码。这个效果更快。
最后,上面把它作为参数传递的解决方案也可以!我更喜欢带有更新 if
块的那个,因为它很容易修改,而且您可以了解它背后的逻辑。
这是我的状态:
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
。它始终是初始状态。我试图通过按钮的 onClick 处理程序调用 onMapClick
。在那里我可以阅读 markers
。如果原始事件在地图上开始,为什么我无法读取 markers
?如何读取 onMapClick
中的状态变量?
这是一个例子:https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js
当您在地图中单击并查看控制台时,您会看到 onMapClick
中的 markers
数组在侦听 markers
的 useEffect
中填充时保持为空.
React 状态是异步的,它不会立即保证给你新的状态,至于你的问题如果原始事件从地图开始,为什么我不能读取标记 它是一种异步性质,并且函数基于它们当前的闭包使用状态值,状态更新将反映在下一个 re-render 中,现有闭包不会受到影响,但会创建新的闭包,这你不会在 class 组件上遇到的问题,因为你有 this 实例,它具有全局范围。
作为开发组件,我们应该确保组件是从您调用它的地方控制的,而不是处理状态的函数闭包,它会在每次状态更改时 re-render。您的解决方案是可行的,您应该在需要时将值传递给您传递给函数的任何事件或操作。
编辑:- 很简单,只需将参数或 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/ ,它将对您提供更长期的帮助 !!!
很长,但您会理解为什么会发生这种情况以及更好的修复方法。闭包尤其是一个问题(也很难理解),主要是当我们设置 依赖于状态 的点击处理程序时,如果具有新作用域的处理程序函数没有重新附加到单击事件,然后闭包保持未更新,因此 陈旧状态保留在单击处理程序函数中。
如果您在组件中完全理解它,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 没有用新函数替换点击处理程序,它 adds another event handler (基本上堆叠event handlers ),因此我们必须在添加新的之前删除旧的,否则每次更新 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 的 link,工作正常。
现在有另一个想法可以解决它,而无需每次都替换点击处理程序。即一些全局变量,我认为这并不是太糟糕。
为此,在您的组件外部但上方添加 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
这个也可以完美地工作,Link 到 sandbox 使用此代码。这个效果更快。
最后,上面把它作为参数传递的解决方案也可以!我更喜欢带有更新 if
块的那个,因为它很容易修改,而且您可以了解它背后的逻辑。