[英]React infinite rerender, useEffect issue, no set state triggered
My component seems to enter an endless loop and I cannot find the reason.我的组件似乎进入了无限循环,我找不到原因。 I am using the
useEffect
and useState
hooks but that is not it.我正在使用
useEffect
和useState
挂钩,但事实并非如此。 To anyone wanting to tag this as duplicate: Please read the issue carefully.对于任何想要将其标记为重复的人:请仔细阅读该问题。 This endless re-render loop scenario only happens if:
这种无休止的重新渲染循环场景仅在以下情况下发生:
mousemove
event after (for the purpose of explenation clarity I've omitted the mousedown
/ mouseup
handlers, but they are essentially the same as the mousemove
handler, triggering a call to every subscribed callback.mousemove
事件(为了清楚起见,我省略了mousedown
/ mouseup
处理程序,但它们与mousemove
处理程序基本相同,触发对每个订阅回调的调用. Here is an example, but be careful it can cause an infinite loop and force you to close the browser tab Codesandbox:https://codesandbox.io/s/infiniteloopissue-lynhw?file=/index.js这是一个示例,但请注意它可能会导致无限循环并强制您关闭浏览器选项卡Codesandbox:https://codesandbox.io/s/infiniteloopissue-lynhw?file=/index.js
I am using Map.forEach
to trigger the setState
of any subscribers on mousemove
event.我正在使用
Map.forEach
在mousemove
事件上触发任何订阅者的setState
。 (You can see the relevant code below), and I am using useEffect
to subscribe/unsubscribe from this "update event" (您可以在下面看到相关代码),我正在使用
useEffect
订阅/取消订阅此“更新事件”
One thing that "fixes" the problem is the following - Check for a Reference Point comment (below in the Mouse Consumer).以下是“解决”问题的一件事 - 检查参考点注释(在鼠标使用者下方)。 If the callback is removed as a dependency of the
useCallback
hook, everything works fine.如果回调作为
useCallback
挂钩的依赖项被删除,则一切正常。 But this is not good, because in this example, we obviously just extract the data into the state, but that callback
function could be dependent on, say some other state, in which case it would not work.但这并不好,因为在这个例子中,我们显然只是将数据提取到 state 中,但是
callback
function 可能依赖于,比如其他一些 state,在这种情况下它不会起作用。 The callback needs to be mutable.回调需要是可变的。
My guess is that the react somehow manages to re-render BEFORE the .forEach
finishes it's iterations, in which case it would unsubscribe (thus removing the key), and re-subscribe (thus adding it again) triggering yet another callback, which then triggers another unsub/resub and we go into a loop.我的猜测是,反应以某种方式设法在
.forEach
完成它的迭代之前重新渲染,在这种情况下,它会取消订阅(从而删除密钥),并重新订阅(从而再次添加它)触发另一个回调,然后触发另一个取消订阅/重新订阅,我们 go 进入一个循环。 But that shouldn't be possible right?但这应该是不可能的吧? I mean javascrip is suppose to be blocking single threaded, how/why does react re-render in the middle of a forEach loop?
我的意思是 javascrip 应该阻塞单线程,如何/为什么在 forEach 循环中间重新渲染反应?
Also, does someone have a better idea on how to "subscribe" to a mousemove
and run the callback.此外,是否有人对如何“订阅”
mousemove
移动并运行回调有更好的想法。 I recently saw some EventEmitter
in some back-end code, but am not familiar with it.我最近在一些后端代码中看到了一些
EventEmitter
,但对它并不熟悉。 Am also not sure if that could fix the issue here, the issue being react takes precedence when updating over waiting for the main thread to finish a .forEach
loop (at least I think that is it)我也不确定这是否可以解决这里的问题,在更新时反应问题优先于等待主线程完成
.forEach
循环(至少我认为是这样)
The base is simple:基础很简单:
const App = () => {
return (
<MouseProvider>
<Component />
</MouseProvider>
)
}
const Component = props => {
const [mouse, setMouse] = useState({})
const callback = data => {
setMouse({ x: data.x, y: data.y })
}
useMouseTracker(callback)
return (
<div>
{`x: ${mouse.x} y: ${mouse.y}`}
</div>
)
}
The idea behind the component is, to write down the current mouse position on screen at all times.该组件背后的想法是,始终在屏幕上记下当前鼠标 position。 This information could be readily available in the context, however in order to render it on the screen, we need to trigger a "ReRender" so Context API is not a solution, instead.
此信息可以在上下文中轻松获得,但是为了在屏幕上呈现它,我们需要触发“重新渲染”,因此 Context API 不是解决方案,而是。
// Static mutable object used.
const mouseData = { x: 0, y: 0 }
// A map of "id : callback" pairs
const subscribers = new Map()
Provider = ({ children }) => {
const mouseMoveHandler = useCallback(event => {
if (event) {
mouseData.x = event.clientX
mouseData.y = event.clientY
subscribers.forEach(callback => {
callback({ ...mouseData})
})
}
}, [])
useEffect(() => {
window.addEventListener('mousemove', mouseMoveHandler)
return () => {
window.removeEventListener('mousemove', mouseMoveHandler)
}
}, [mouseMoveHandler])
return (
<React.Fragment>
{children}
</React.Fragment>
)
}
So every time the user moves his mouse, the mousemove
handler will update the static
object.因此,每次用户移动鼠标时,
mousemove
处理程序都会更新static
object。 The Provider component itself DOES NOT rerender. Provider 组件本身不会重新渲染。
useMouseTracker = callback => {
const id = 0 // This value is not 0, it's a system-wide per-component constant, guaranteed, tried and tested
const subscribe = useCallback(() => {
subscribers.set(id, callback)
}, [id, callback /* Reference Point */])
const unsubscribe = useCallback(() => {
subscribers.delete(id)
}, [id])
useEffect(() => {
subscribe()
return unsubscribe
}, [subscribe, unsubscribe])
}
As we can see, the Consumer Hook implements two functions, which subscribe
and unsubscribe
the id
into the Map of callbacks, previously referenced in the Provider, that is ALL it does, it doesn't call the callback, it never triggers anything other then adding/removing the callback
from the Map object.正如我们所见,Consumer Hook 实现了两个函数,将
id
subscribe
和unsubscribe
到回调的 Map 中,之前在 Provider 中引用过,这就是它所做的一切,它不调用回调,它永远不会触发其他任何东西从 Map object 添加/删除callback
。 All of the "updating" is done by the Provider or rather the component who's callback the provider calls, on every mousemove
.所有的“更新”都是由提供者完成的,或者更确切地说是回调提供者调用的组件,在每个
mousemove
上。 In other words, the useEffect
doesn't EVER trigger a state update, mouse interaction does.换句话说,
useEffect
不会触发 state 更新,鼠标交互会。
The useEffect
hook returns an unsubscribe function which makes sure it "cleans up after itself" so that the callback
doesn't get triggered if the component is dismounted. useEffect
挂钩返回一个取消订阅 function 以确保它“自行清理”,以便在卸载组件时不会触发callback
。
Well as I said the problem is, I end up in an endless re-render loop with this component, and it only happens if the mouse is moved too fast or if it goes "offscreen" such as into the developer console, for example.好吧,正如我所说的,问题是,我最终会使用这个组件进行无休止的重新渲染循环,并且只有当鼠标移动得太快或者它“离开屏幕”(例如进入开发人员控制台)时才会发生这种情况。
EDIT: removed context entirely, was not necessary and was causing confustion.编辑:完全删除上下文,没有必要并且造成混乱。 EDIT2: added codesandbox
EDIT2:添加了代码框
Split subscribe
and unsubscribe
to two different useEffect
calls:拆分
subscribe
和unsubscribe
两个不同useEffect
调用:
useEffect(() => {
subscribe()
}, [subscribe])
and和
useEffect(() => unsubscribe, [unsubscribe])
This prevents the callback from being removed and recreated, but still cleans up if the component it unmounted.这可以防止回调被删除和重新创建,但如果它卸载了组件,仍然会清理它。 It does seem a bit kludgey, but it does seem to work.
它看起来确实有点笨拙,但它似乎确实有效。
According to the react docs :根据反应文档:
The clean-up function runs before the component is removed from the UI to prevent memory leaks.
清理 function 在从 UI 中删除组件之前运行,以防止 memory 泄漏。 Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect .
此外,如果一个组件多次渲染(通常是这样),则在执行下一个效果之前会清除上一个效果。
(eg when the effect dependencies change) (例如当效果依赖改变时)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.