React16升级到17的一个坑:组件销毁时Ref可能会重置为Null最近公司项目使用React从16版本升级到17版本,选择升级的原因是为了以后将项目迁移到Nextjs.结果证明这是一场灾难,由于React的不一致行为,出现了可见和不可见的错误。React17是一个特殊的版本,它没有任何新特性,但是它对React的底层进行了改造,让React17可以逐步升级一些模块,为18版本做准备。其中一些改造是破坏性的。问题我这里有一个弹窗,弹窗里面有一些表单项。当用户对表单项进行一些修改,然后点击弹窗的遮罩层时,里面的组件就会被销毁。在销毁时,我们调用ref的save()方法来保存数据。useEffect(()=>{return()=>{formRef.current&&formRef.current.save();}},[]);在React16是正常的,但是在React17就失败了,我们无法保存表单中的数据。经过一番排查,发现问题所在:在React17版本中,组件销毁时获取的ref.current可能会重置为null。然后我找到了这种情况的官方文档:https://zh-hans.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timinguseEffectcleanuptiminguseEffect(()=>{return()=>{//在这里执行清理}})在React17中,副作用的执行时间发生了变化。一个破坏性的影响是:如果组件被卸载,sideeffectcleanup的时机是异步的,相应的回调函数的执行也是异步的。是的,异步的。卸载时要执行的回调函数访问状态和方法没有问题。它们是不可变的,可以通过闭包访问。但是问题是ref,它是可变的,我们可以随意设置ref.current的值,不会触发组件的重新渲染。当组件卸载时,这个ref将被React重置为null。因为它是异步的,所以我们很可能会得到一个空值。这里有一个简单的在线demo,有兴趣的可以看看,Component组件销毁时,elRef变为null:https://codesandbox.io/s/react-17-zhong-zu-jian-xiao-hui-shi-ref-ke-neng-bei-she-zhi-wei-null-2kl4xu。然后是React16版本的ref,因为是同步的,所以销毁时ref不会重置为null:https://codesandbox.io/s/16-de-ref-shi-zheng-chang-de-18平方米。解决方案官方文档提供了两种解决方案。一种是使用useLayoutEffect。useLayoutEffect(()=>{return()=>{formRef.current&&formRef.current.save();}},[]);useLayoutEffect可以保证回调函数是同步执行的,这样可以保证此时ref仍然是最后一个值,而不是被设置为null。第二种方式是每次ref变化时,用一个临时变量保存ref.current,放在sideeffectcleanup回调函数的闭包中,保证不变性。useEffect(()=>{constinstance=someRef.current;instance.someSetupMethod();return()=>{instance.someCleanupMethod();};});但是这里好像还有一个限制:第二个参数,也就是dependency参数,因为我们不能保证ref在中间没有变化。目前,我正在使用第一种解决方案来处理我遇到的问题。对于最终的版本升级,我们还是要权衡利弊。
