简体   繁体   English

React 无限重新渲染,useEffect 问题,未触发设置 state

[英]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.我正在使用useEffectuseState挂钩,但事实并非如此。 To anyone wanting to tag this as duplicate: Please read the issue carefully.对于任何想要将其标记为重复的人:请仔细阅读该问题。 This endless re-render loop scenario only happens if:这种无休止的重新渲染循环场景仅在以下情况下发生:

  1. The mouse is moved too fast鼠标移动太快
  2. A user clicks anything inside the developer console and then triggers a 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.forEachmousemove事件上触发任何订阅者的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:基础很简单:

App.js应用程序.js

const App = () => {
  return (
    <MouseProvider>
        <Component />
    </MouseProvider>
  )
}

Component.js组件.js

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 不是解决方案,而是。

Mouse Provider鼠标提供者

//  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 组件本身不会重新渲染。

Mouse Consumer鼠标消费者

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 subscribeunsubscribe到回调的 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

The Problem问题

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:拆分subscribeunsubscribe两个不同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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM