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

老大,为什么第三方组件的Hooks会报错呢?

时间:2023-03-18 17:17:01 科技观察

最近在工作中遇到了一个有趣的问题,记录一下从发现问题到解决问题的过程。本题涉及的知识点包括:hooks源码逻辑package.json配置某个需求需要引入第三方组件库。当引入组件库中的函数组件A时,React运行时报错:“Invalidhookcall.Hookscanonlybecalledinsideofafunctioncomponent.Thiscouldhappeninsideoneofthesereasons...从React文档了解到这是由于“错误使用Hooks”造成的,官网给出的错误可能有以下三种原因:1.React与ReactDOM版本不匹配需要ReactDOMv16.8以上版本支持Hooks.我们项目使用的是v17.0.2,不属于这个原因2.打破Hooks的规则Hooks只能在函数组件顶层调用或者自定义Hooks看组件A的源码,报错reported是一个top-levelcalleduseRef:functionA(){//...varxxxRef=useRef(null);//...}不属于这个原因。3.React文档中记录了重复的React:在为了让Hooks正常工作,应用程序代码中的反应依赖项和t在react-dom包中使用的react依赖项必须解析为同一个模块。如果这些反应依赖项解析为两个不同的导出,您将看到此警告。如果你不小心包含了react包的两个副本,就会发生这种情况。读来扑朔迷离,似乎这一篇最为可疑。定位问题。在报错的useRef打断点,发现来自于:http://localhost:8081/Users/projectdirectory/node_modules/componentlibrary/node_modules/react/cjs/react.development.js中的其他调用项目Hooks在没有报错的地方打断点,发现资源来自:http://localhost:8081/Users/projectdirectory/node_modules/react/cjs/react.development.js的useRef错误报告和项目的其他Hooks引用了不同的react.development.js。查看“组件库”的package.json,发现他安装了react和react-dom作为依赖:"dependencies":{"react":"^16.13.1","@babel/runtime-corejs3":"^7.11.2","re??act-dom":"^16.13.1"},这会在“ComponentLibrary”目录下的node_modules下创建这两个依赖。作为一个“组件库”,这样做显然是不合适的。暂时解决的最好办法就是把这两个依赖作为peerDependencies,也就是作为外部依赖。这样,当我们导入“组件库”时,“组件库”就会使用我们项目中的react和react-dom,而不是自己安装一个。但是我没有这个“组件库”的权限,只能在自己的项目上做文章。package.json文件中提供了一个配置项:resolutions,可以暂时解决这个问题。resolutions允许您覆盖嵌套??在项目node_modules中引用的包的版本。在我们项目的package.json中进行以下更改://projectpackage.json{//..."resolutions":{"react":"17.0.2","re??act-dom":"17.0.2"},//...}这样,项目中使用的两个依赖都会使用resolutions中指定的版本。无论是我们项目代码中的“组件库”还是react、react-dom,都会指向同一个文件。现在问题暂时解决了,但是问题的原因是什么?让我们深入Hooks的源码寻找答案。深入源码,首先让我们思考两个问题:当我们在一个Hook内部调用其他Hook时,会报一开始提到的错误。比如下面的代码会报错:functionApp(){useEffect(()=>{consta=useRef();},[])//...}Hooks只是函数,他怎么感知他是在另一个Hooks内部执行的吗?如上例,useRef是如何感知自己在useEffect的回调函数中执行的呢?看另一个问题,我们知道classComponent有两个生命周期函数,componentDidMount和componentDidUpdate,用来区分mount和update。那么Hooks作为一个函数,如何区分当前是挂载还是更新呢?很显然,Hooks源码内部有一种机制可以感知当前的执行上下文。越来越好在浏览器环境中,我们会提到react和reactDOM这两个包。其中,react包的代码中有一个变量ReactCurrentDispatcher。他的current参数指向当前使用的Hooks上下文:varReactCurrentDispatcher={/***@internal*@type{ReactComponent}*/current:null};同时,在reactDOM中,程序运行过程中,ReactCurrentDispatcher.current会根据当前上下文指向不同的引用。例如:varHooksDispatcherOnMountInDEV={useState:function(){//...},useEffect:function(){//...},useRef:function(){//...},//...}varHooksDispatcherOnUpdateInDEV={useState:function(){//...},useEffect:function(){//...},useRef:function(){//...},//...}//...当挂载在DEV环境中时,ReactCurrentDispatcher.current将指向HooksDispatcherOnMountInDEV。在DEV环境更新时,ReactCurrentDispatcher.current会指向HooksDispatcherOnUpdateInDEV。我们看useRef的定义:functionuseRef(initialValue){vardispatcher=resolveDispatcher();returndispatcher.useRef(initialValue);}内部调用是dispatcher.useRef。调度程序是ReactCurrentDispatcher.current。functionresolveDispatcher(){vardispatcher=ReactCurrentDispatcher.current;if(!(dispatcher!==null)){{throwError("Invalidhookcall....");}}returndispatcher;}可以看到,一开始的错误是由于dispatcher为null时会抛出,这就是Hooks可以区分挂载和更新的原因。同样,在DEV环境下,当一个Hooks在执行时,ReactCurrentDispatcher.current会指向引用——InvalidNestedHooksDispatcherOnUpdateInDEV。在这种情况下,再次调用的Hooks,比如下面的useRef:},//...}会执行warnInvalidHookAccess报错,提醒自己在其他Hooks中执行。现在真相大白,我们终于知道了开头提到的问题根源:因为“组件库”使用的是dependencies而不是peerDependencies,所以“组件库”中引用的react和reactDOM都是“component”下的文件库”目录node_modules.项目中使用的react和reactDOM是项目目录node_modules下的文件。“组件库”中的react和项目目录中的react在运行时初始化了ReactCurrentDispatcher。这两个ReactCurrentDispatcher分别依赖对应目录的reactDOM。我们在项目中的project目录下执行reactDOM的ReactDOM.render方法,它会随着程序运行改变项目。目录下react包下的ReactCurrentDispatcher.current指向“组件库”中的ReactCurrentDispatcher.currentisalwaysnull调用“组件库”中的Hooks时,由于ReactCurrentDispatcher.current始终为null而报错。总结通过分析这个问题,加深对package.json源码和Hooks的理解。不知道Hooks-awarecontext的实现思路对你有没有启发?