编程

React 报错:Can't perform a React state update on an unmounted component

1800 2023-03-21 23:05:00

网上搜索得出的千遍一律解决思路:找到导致报错的setState代码,能去掉的去掉。不能去掉的,设置isMounted标志来保证setState时,组件处于已挂载的状态。这种hack方式来抑制警告,吃力不讨好,且其实已经被React核心成员Dan否定了。

最直接有效的方式:

React开发里遇到Warning: Can't perform a React state update on an unmounted component报错,将React版本升级到17+,报错错误即可消失。

有精力的,可以看看以下一堆解(废)释(话):

做过React开发,都常常遇到一个典型的React问题:Warning: Can't perform a React state update on an unmounted component

详细错误:Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

当试图在一个unmounted(未挂载/已卸载)组件上执行setState时,React会抛出此错误,提示有内存泄漏的风险。

 

为什么React要这么设计呢?

官方出发点也是为了防止内存泄漏。看看React官方最开始考虑的案例:

useEffect(() => {
  function handleChange() {
     setState(store.getState())
  }
  store.subscribe(handleChange)
  return () => store.unsubscribe(handleChange)
}, [])

理想的状态是:在UseEffect里做事件订阅,在清理副作用阶段(UseEffect的返回方法里)取消事件订阅。

此时用户若忘记取消订阅,则事件监听未被取消,造成内存泄漏。

不过这个这个错误提示在React17+已不再存在了。官方认为useEffect里取消事件订阅场景并非普遍,更多是在第三方库或者自定义hooks里用到。

在实际场景里,有更普遍的例子:

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}

这种情况会触发报错。但此时报错有误导性,因为这种情况并不会发生内存泄漏,未被处理的promise会被浏览器垃圾回收机制自动清理。

针对“Warning: Can't perform a React state update on an unmounted component”报错问题,网上给出了很典型普遍流传的解决方案:

useEffect(() => {
  let isMounted = true;               // note mutable flag
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // 保证setState时,组件处于挂载状态
  })
  return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);      

每次都得花时间去定位具体是哪个"setState"导致,然后使用此hack方式处理。但是,此修复程序实际上并没有解决任何“内存泄漏”。它只是抑制警告,只是看不到报错而已。比如之前的store依然没有unsubscribe。

如前所述,无论如何这里也没有实际泄漏。等待 Promise 是暂时的,没有什么可以无限期地保留组件。这种解决方法不仅无必要,反而比原来的问题更糟糕。

在未来:

1.React将支持在组件不可见的情况下保留DOM和state,但断开相关联的副作用。此时,上面的解决方式就会失效,因为当组件不可见时isMounted为false,但按照新的设计,React是可以继续setState的。

2.这个报错会“诱导”开发者使用上述useEffect()的清理机制加一个isMounted标志来抑制警告。实际场景中用户操作(比如这里的POST请求)可能只是需要绑定一个事件由事件触发即可。也就是该报错间接地“误导”了开发者的使用方式,导致开发者一股脑地用useEffect。而useEffect的一个风险是一旦依赖的数据改变会导致内部的用户操作被多次执行,导致程序变脆弱了。

所以,React核心成员Dan在讨论里提到移除这个报错。原因还上面提的到,取消事件订阅的场景并不多。大多数产品开发人员遇到的情况是此警告不仅无用/具有误导性,而且还会促使开发人员采取更糟糕的解决方案(useEffect+isMounted标志来抑制警告)。移除之后,反而减少此问题带来的困惑,从此useEffect+isMounted标志的方案也就不会再被使用了。

当然移除这个报错只是一个权衡之下的方案,减少更为普遍的困惑。事件未取消订阅导致内存泄漏问题仍存在,仍需要开发者手动取消订阅。

所以,升级V17后,可以继续在unMount的组件上setState了。