react-refreshreact-refresh-webpack-plugin[1]是React官方提供的模块热替换(HMR)插件。一个Webpack插件,用于为React组件启用“快速刷新”(以前也称为热重新加载)。在开发环境编辑代码时,react-refresh可以保持组件当前状态,只改变编辑的部分。在umi[2]中,可以通过fastRefresh:{}快速开启该功能。这个gif动图展示了使用react-refresh特性的开发体验。可以看到,修改组件代码后,已经填写的用户名和密码保持不变,只改变了编辑的部分。react-refresh的简单原理对于Class组件,react-refresh会重新挂载(remount)所有的组件,现有的状态会被重置。对于功能组件,react-refresh将保留现有状态。所以react-refresh对于功能组件会有更好的体验。本文主要讲解React-refresh模式下ReactHooks的怪异行为。下面我来看一下react-refresh对于函数组件的工作机制。为了保持热更新时的状态,useState和useRef的值不会更新。热更新时,为了解决一些问题[3],会重新执行useEffect、useCallback、useMemo等。当我们更新代码时,我们需要“清理”保留过去值的效果(例如传递的函数),并使用更新后的值“设置”新的效果。否则,您的效果使用的值将是陈旧的并且与您渲染中使用的值“不一致”,这使得快速刷新的用处大大降低,并损害了让它与自定义挂钩链一起工作的能力。.react-refresh的工作机制带来的问题在上面的工作机制下,会出现很多问题。下面我举几个具体的例子。第一题importReact,{useEffect,useState}from'react';exportdefault()=>{const[count,setState]=useState(0);useEffect(()=>{setState(s=>s+1);},[]);return(
{count}
)}上面的代码很简单,在普通模式下,count的最大值为1。因为useEffect在初始化的时候只会执行一次。但是在react-refresh模式下,每次热更新的时候state都没有变化,但是useEffect的重新执行会导致count的值一直增加。如上图所示,count随着每次热更新而递增。第二个问题是,如果使用ahooks[4]或者react-use[5]的useUpdateEffect,在热更新模式下会有意想不到的行为。importReact,{useEffect}from'react';importuseUpdateEffectfrom'./useUpdateEffect';exportdefault()=>{useEffect(()=>{console.log('执行的useEffect');},[]);useUpdateEffect(()=>{console.log('executeuseUpdateEffect');},[]);return(
helloworld
)}useUpdateEffect与useEffect相比,会忽略第一次执行,只有在deps改变时才执行.在上面代码的普通模式下,useUpdateEffect永远不会被执行,因为deps是一个空数组,永远不会改变。但是在react-refresh模式下,热更??新的时候,useUpdateEffect和useEffect是同时执行的。出现这个问题的原因是useUpdateEffect使用ref来记录是否是第一次执行,看下面的代码。import{useEffect,useRef}from'react';constuseUpdateEffect:typeofuseEffect=(effect,deps)=>{constisMounted=useRef(false);useEffect(()=>{if(!isMounted.current){isMounted.current=true;}else{returneffect();}},deps);};exportdefaultuseUpdateEffect;上面代码的关键在于isMounted初始化时执行useEffect,热更新标记isMounted为true后重新执行useEffect,而此时isMounted为true,那么往下看第三个问题,一开始发现了这个问题。ahooks的useRequest热更新后,loading会一直为true。经分析,原因是使用isUnmountref标记组件是否卸载。importReact,{useEffect,useState}from'react';functiongetUsername(){console.log('requested')returnnewPromise(resolve=>{setTimeout(()=>{resolve('test');},1000);});}exportdefaultfunctionIndexPage(){constisUnmount=React.useRef(false);const[loading,setLoading]=useState(true);useEffect(()=>{setLoading(true);getUsername().then(()=>{if(isUnmount.current===false){setLoading(false);}});return()=>{isUnmount.current=true;}},[]);returnloading?
loading
:
helloworld
;}如上代码所示,热更新时isUnmount变为true,导致代码认为组件已经卸载,再次执行时不再响应异步操作。如何解决这些问题解决方案一第一种解决方案是从代码层面解决,即要求我们在写代码的时候时不时的记住react-refresh模式下的怪异行为。例如,使用useUpdateEffect,我们可以在初始化或热替换时初始化isMountedref。如下:import{useEffect,useRef}from'react';constuseUpdateEffect:typeofuseEffect=(effect,deps)=>{constisMounted=useRef(false);+useEffect(()=>{+isMounted.current=false;+},[]);useEffect(()=>{if(!isMounted.current){isMounted.current=true;}else{returneffect();}},deps);};exportdefaultuseUpdateEffect;此方案针对上述问题两个和所有三个都有效。解决方案2根据官方文档[6],我们可以通过在文件中添加如下注释来解决这个问题。/*@refreshreset*/添加这个问题后,每次热更新都会重新挂载,也就是重新执行组件。useState和useRef也会被重置,所以不会出现上面的问题。官方态度本来ReactHooks的潜规则还是挺多的。在使用react-refresh的时候,还是有一些潜规则需要注意的。但是官方回复说这是预期的行为,参见这个问题[7]。效果不完全是“安装”/“卸载”——它们更像是“显示”/“隐藏”。不管你晕不晕,反正我是,╮(╯▽╰)╭。参考资料[1]react-refresh-webpack-plugin:https://github.com/pmmmwh/react-refresh-webpack-plugin[2]umi:https://umijs.org/zh-CN/docs/fast-refresh[3]为了解决一些问题:https://github.com/facebook/react/issues/21019#issuecomment-800650091[4]ahooks:https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUpdateEffect/index.ts[5]react-use:https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md[6]官方文档:https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#reset[7]问题:https://github.com/facebook/react/issues/21019