当前位置: 首页 > 科技观察

Ahooks的UseClickAway在React17中不起作用,怎么办?

时间:2023-03-13 13:45:17 科技观察

最近公司前端项目从React16升级到React17,导致ahooks的useClickAway无法正常使用。下面西瓜哥给大家说说事情的经过。ahooks中的useClickAwayahooks是阿里巴巴维护的第三方ReactHook库,封装了很多有用的hooks。比如useMount和useUnmount用于挂载和卸载经常使用的组件,还有useRequest支持自动请求、手动请求、防抖等功能请求,useLocalStorageState可以同步localStorage的状态访问。当你想写一个与业务无关的第三方ahooks时,你可以在ahooks中寻找。你很有可能找到它。这是一个优秀的钩子库。其中useClickAway的作用是监听目标元素外的点击事件。useClickAway接受的第一个参数是一个事件回调函数。第二个参数是要排除的目标元素,可以是ref或DOM元素,也可以是它们的数组,第三个是要监听的事件类型字符串或事件字符串数组。第三个参数是可选的,如果不使用,默认使用点击事件'click'。下面是常用的写法:useClickAway(()=>{console.log('点击到元素外的地方');},ref);useClickAway的核心底层原理是在文档上绑定了一个冒泡事件。当事件冒泡到document时,会判断事件目标元素是否是传入ref下的子元素。如果是,什么也不做。如果没有,执行回调函数。这里是useClickAway的源码地址。有兴趣的可以研究一下:https://github.com/alibaba/hooks/blob/v3.5.0/packages/hooks/src/useClickAway/index.ts。useClickAway的问题如果你在React16中使用useClickAway,一切正常。但是如果在React17及以上版本使用,在某些情况下会出现问题。我们有这样的场景。点击一个搜索按钮,会出现一个输入框,用户需要在这个输入框中输入文字进行搜索。如果单击搜索按钮以外的地方,输入框就会消失。核心实现如下:functionApp(){const[visible,setVisible]=useState(false);constinputRef=useRef();useClickAway(()=>{setVisible(false);},inputRef);return(

setVisible(true)}>Search{visible&&}
);}这是一个在线的demo(使用的是React17版本):https://codesandbox.io/s/f54siy。在React16中,上面的写法是正常的。但是升级到17后,你会发现点击按钮后没有任何反应。React17的事件系统改造原因是React17对事件系统进行了改造。从16升级到17后,React将事件委托给ReactDOM挂载的根节点,比如div#app,而不是原来的document。首先,我们需要知道的是,调用setVisible(true)改变组件状态时,会立即重新渲染组件,然后调用useClickAway。状态更新后组件的重新渲染是同步的,我们的事件流还没有结束。需要注意的是,更新状态后组件的重新渲染可能是同步的,也可能是异步的。在React16中,事件被委托给文档。我们点击按钮元素来生成一个事件流。当点击事件流向文档时,我们设置visible为true,组件进行同步重渲染,并调用useClickAway对文档进行冒泡事件绑定。.像这样:document.addEventListener('click',()=>{console.log('displayinputbox')//React16中useClickAway绑定事件的时机document.addEventListener('click',()=>{console.log('隐藏输入框');});});//点击后输出内容为://在某个元素的事件触发过程中显示输入框,在该元素类型上注册一个新的相同元素事件响应函数,这个新的响应函数不会在这个事件流上立即被触发。所以,之前的useClickAway在React16中是正常的,但是在React17中就不一样了,事件委托委托给了div#app。点击按钮,事件流冒泡到div#app元素,执行事件回调函数将visible设置为true,并重新渲染组件,执行useClickAway并为文档绑定一个新的事件响应函数。这时候事件流还没有结束,继续冒泡到document,把visible设置回false。因此,visible短暂变为true,然后返回false,什么也没有发生。document.querySelector('#app').addEventListener('click',()=>{console.log('displayinputbox')//React17中useClickAway绑定事件的时机document.addEventListener('click',()=>{console.log('Hiddeninputbox');});});//点击后输出为://显示输入框//隐藏输入框方案一:防止冒泡{e.stopPropagation();设置可见(真);}}>我们给按钮添加防止事件冒泡,提前结束事件流,使其不流向文档,就不会触发文档的点击事件。但这也是一个隐患,e.stopPropagation是破坏性的。如果我们要在其他地方写一些特殊的失焦判断逻辑,也需要用到类似useClickAway的方法。当我们点击这个按钮的时候,其他地方的逻辑就不起作用了。溢出:隐藏;在CSS中也是破坏性的,如果设置了这个属性的容器内的元素超出了容器的边界,就会被截断。方案二:修改绑定事件类型为mousedown/touchstartuseClickAway(()=>setVisible(false),inputRef,['mousedown','touchstart']);mousedown在点击事件之前结束,所以在点击事件流程中不会触发它。touchstart是为了与移动设备兼容。因为触摸屏幕的时候肯定会触发touchstart,mousedown不一定,顺便说一下click也不一定。其他优秀的第三方ReactHooks库,例如react-use的useClickAway,实际上使用mousedown和touchstart作为默认事件类型。还有百度的react-hooks库。其下的useClickOutside不支持自定义事件类型,但也使用了mousedown和touchstart。解决方案3:将按钮元素传递给useClickAwayuseClickAway(()=>setVisible(false),[inputRef,buttonRef]);这样,按钮也可以排除在触发条件之外。但是这样写很麻烦。如果要将输入框封装到组件中,则必须将buttonRef传递到该组件中。方案四:延迟输入框的计时{setTimeout(()=>{setVisible(true);});}}>通过setTimeout,确保输入框出现在同步事件流中,然后触发useClickAway绑定逻辑。最后,React16升级到17后,React中的混合事件托管绑定到挂载在React组件树上的div#app,而不是之前的文档。这使得默认注册为点击事件类型的useClickAway在某些场景下的行为与React16不同。对于以上场景和解决方案,我认为最好的是第二种:将useClickAway的事件类型设置为mousedown和touchstart。这种方法更具有普适性。