vieyahn2017 / jsup

0 stars 1 forks source link

[7.27] 第三方组件的hooks为啥报错了? #16

Closed vieyahn2017 closed 11 months ago

vieyahn2017 commented 11 months ago

cnpm i -g create-react-app npx create-react-app myreact1 / 慢 改用下面的 / cnpm init react-app my-app

cnpm install react-schema-render -S cnpm i antd

npm run start

vieyahn2017 commented 11 months ago

报错

react.development.js:209 Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. printWarning @ react.development.js:209 react.development.js:1618 Uncaught TypeError: Cannot read properties of null (reading 'useContext') at useContext (react.development.js:1618:1) at index.esm.js:309:1 at renderWithHooks (react-dom.development.js:16305:1) at updateFunctionComponent (react-dom.development.js:19588:1) at updateSimpleMemoComponent (react-dom.development.js:19425:1) at updateMemoComponent (react-dom.development.js:19284:1) at beginWork (react-dom.development.js:21673:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1) at invokeGuardedCallback (react-dom.development.js:4277:1)

2react.development.js:209 Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem. printWarning @ react.development.js:209 react.development.js:1618 Uncaught TypeError: Cannot read properties of null (reading 'useContext') at useContext (react.development.js:1618:1) at index.esm.js:309:1 at renderWithHooks (react-dom.development.js:16305:1) at updateFunctionComponent (react-dom.development.js:19588:1) at updateSimpleMemoComponent (react-dom.development.js:19425:1) at updateMemoComponent (react-dom.development.js:19284:1) at beginWork (react-dom.development.js:21673:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1) at invokeGuardedCallback (react-dom.development.js:4277:1) react-dom.development.js:18687 The above error occurred in one of your React components:

    at http://localhost:3000/static/js/bundle.js:36011:21 at App

Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries. logCapturedError @ react-dom.development.js:18687 react-dom.development.js:26923 Uncaught TypeError: Cannot read properties of null (reading 'useContext') at useContext (react.development.js:1618:1) at index.esm.js:309:1 at renderWithHooks (react-dom.development.js:16305:1) at updateFunctionComponent (react-dom.development.js:19588:1) at updateSimpleMemoComponent (react-dom.development.js:19425:1) at updateMemoComponent (react-dom.development.js:19284:1) at beginWork (react-dom.development.js:21673:1) at beginWork$1 (react-dom.development.js:27426:1) at performUnitOfWork (react-dom.development.js:26557:1) at workLoopSync (react-dom.development.js:26466:1)

vieyahn2017 commented 11 months ago

开启debug 下面的dispatcher确实是null


function resolveDispatcher() {
  var dispatcher = ReactCurrentDispatcher.current;

  {
    if (dispatcher === null) {
      error('Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.');
    }
  } // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.

  return dispatcher;
}
function useContext(Context) {
  var dispatcher = resolveDispatcher();

  {
    // TODO: add a more generic warning for invalid values.
    if (Context._context !== undefined) {
      var realContext = Context._context; // Don't deduplicate because this legitimately causes bugs
      // and nobody should be using this in existing code.

      if (realContext.Consumer === Context) {
        error('Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' + 'removed in a future major release. Did you mean to call useContext(Context) instead?');
      } else if (realContext.Provider === Context) {
        error('Calling useContext(Context.Provider) is not supported. ' + 'Did you mean to call useContext(Context) instead?');
      }
    }
  }

  return dispatcher.useContext(Context);
}
vieyahn2017 commented 11 months ago

在知乎这边搜到了专业的解答

https://zhuanlan.zhihu.com/p/363288266

无法复制,采用下面的: read:https://zhuanlan.zhihu.com/p/363288266 自动变成: read://https_zhuanlan.zhihu.com/?url=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F363288266

vieyahn2017 commented 11 months ago

大佬,第三方组件的hooks为啥报错了? 最近工作中遇到个有意思的问题,记录下从问题发现到解决的过程。

这个问题涉及知识点包括:

hooks源码逻辑 package.json配置 事发 某个需求需要引入一个第三方组件库。

当引入组件库中的函数组件A后,React运行时报错:

"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons... 从React文档了解到,这是由于「错误使用Hooks造成的」。

官网给出的可能的错误原因有3种:

React和ReactDOM版本不匹配 需要v16.8以上版本的ReactDOM才支持Hooks。

我们项目使用的是v17.0.2,不属于这个原因。

打破了Hooks的规则 Hooks只能在函数组件或自定义Hooks顶层调用。

翻看A组件源码,报错的是一个顶层调用的useRef:

function A() { // ... var xxxRef = useRef(null); // ... } 不属于这个原因。

重复的React 载录自React文档:

为了使 Hook 正常工作,你应用代码中的 react 依赖以及 react-dom 的 package 内部使用的 react 依赖,必须解析为同一个模块。

如果这些 react 依赖解析为两个不同的导出对象,你就会看到本警告。这可能发生在你意外地引入了两个 react 的 package 副本。 读起来好绕,看起来这条的嫌疑最大。

定位问题 在报错的useRef中打上断点,发现其来自于:

http://localhost:8081/Users/项目目录/node_modules/组件库/node_modules/react/cjs/react.development.js

在项目里其他调用Hooks但是未报错的地方打上断点,发现资源来自于:

http://localhost:8081/Users/项目目录/node_modules/react/cjs/react.development.js

报错的useRef和项目其他Hooks引用了不同的react.development.js。

翻看「组件库」的package.json,发现他将react与react-dom作为dependencies安装:

"dependencies": { "react": "^16.13.1", "@babel/runtime-corejs3": "^7.11.2", "react-dom": "^16.13.1" }, 这样会在「组件库」目录的node_modules下创建这两个依赖。

作为一个「组件库」,这么做显然是不合适的。

临时解决 最好的做法是将这两个依赖作为peerDependencies,即将其作为外部依赖。

这样,当我们引入「组件库」时,「组件库」会使用我们项目中的react与react-dom,而不是自己安装一份。

但是我没有这个「组件库」的权限,只能在自己项目中做文章。

package.json文档中提供了一个配置项:resolutions,可以临时解决这个问题。

resolutions允许你复写一个在项目node_modules中被嵌套引用的包的版本。

在我们项目的package.json中作出如下修改:

// 项目package.json { // ... "resolutions": { "react": "17.0.2", "react-dom": "17.0.2" }, // ... } 这样,项目中用到的这两个依赖都会使用resolutions中指定的版本。

不管是「组件库」还是我们的项目代码中的react与react-dom,都会指向同一个文件。

现在问题是临时解决了,但是造成问题的原因是什么?

让我们深入Hooks源码内部来寻找答案。

深入源码 首先让我们思考2个问题:

当我们在一个Hooks内部调用其他Hooks时会报开篇提到的错误。

比如如下代码就会报错:

function App() {

useEffect(() => { const a = useRef(); }, [])

// ... } Hooks只是函数,他如何感知到自己在另一个Hooks内部执行?

就如上例子,useRef如何感知到自己在useEffect的回调函数中执行?

再看另一个问题,我们知道classComponent有componentDidMount与componentDidUpdate两个生命周期函数区分mount时与update时。

那么Hooks作为函数,怎么区分当前是mount时还是update时?

显然,Hooks源码内部存在一种机制,能够感知当前执行的上下文环境。

渐入佳境 在浏览器环境,我们会引用react与reactDOM两个包。

其中,在react包的代码中存在一个变量ReactCurrentDispatcher。

他的current参数指向当前正在使用的Hooks上下文:

var ReactCurrentDispatcher = { /**

比如:

var HooksDispatcherOnMountInDEV = { useState: function() { // ... }, useEffect: function() { // ... }, useRef: function() { // ... }, // ... } var HooksDispatcherOnUpdateInDEV = { useState: function() { // ... }, useEffect: function() { // ... }, useRef: function() { // ... }, // ... } // ... 当处在DEV环境mount时,ReactCurrentDispatcher.current会指向HooksDispatcherOnMountInDEV。

当处在DEV环境update时,ReactCurrentDispatcher.current会指向HooksDispatcherOnUpdateInDEV。

再来看useRef的定义:

function useRef(initialValue) { var dispatcher = resolveDispatcher(); return dispatcher.useRef(initialValue); } 内部调用的是dispatcher.useRef。

dispatcher即ReactCurrentDispatcher.current。

function resolveDispatcher() { var dispatcher = ReactCurrentDispatcher.current;

if (!(dispatcher !== null)) { { throw Error( "Invalid hook call. ..." ); } }

return dispatcher; } 可以看到,开篇的错误正是由于dispatcher为null时抛出 这就是Hooks能区分mount与update的原因。

同理,DEV环境,当一个Hooks在执行时,ReactCurrentDispatcher.current会指向引用 —— InvalidNestedHooksDispatcherOnUpdateInDEV。

在这种情况下再调用的Hooks,比如如下useRef:

var InvalidNestedHooksDispatcherOnUpdateInDEV = { // ... useRef: function (initialValue) { currentHookNameInDev = 'useRef'; warnInvalidHookAccess(); updateHookTypesDev(); return updateRef(); }, // ... } 内部都会执行warnInvalidHookAccess报错,提示自己在别的Hooks内执行了。

真相大白 到这里我们终于知道开篇提到的问题发生的本质原因:

由于「组件库」使用dependencies而不是peerDependencies,导致「组件库」中引用的react与reactDOM是「组件库」目录node_modules下的文件。 项目中使用的react与reactDOM是项目目录node_modules下的文件。 「组件库」中react与项目目录中react在运行时分别初始化ReactCurrentDispatcher 这两个ReactCurrentDispatcher分别依赖对应目录的reactDOM 我们在项目中执行项目目录下reactDOM的ReactDOM.render方法,他会随着程序运行改变项目目录中react包下的ReactCurrentDispatcher.current的指向 「组件库」中的ReactCurrentDispatcher.current始终是null 当调用「组件库」中的Hooks时,由于ReactCurrentDispatcher.current始终是null导致报错 总结 通过分析这个问题,加深了对package.json以及Hooks源码的理解。

不知道Hooks感知上下文的实现思路对你有没有启发呢?

vieyahn2017 commented 11 months ago

我用create-react-app创的项目,版本依赖是

"react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1",

引入的第三方组件 react-schema-render

"peerDependencies": { "react": "^16.8.6" },

vieyahn2017 commented 11 months ago

方案一, 把我自己的依赖改到16.8.6 npm cache clean --force npm i 但是报错 Module not found: Error: Can't resolve 'react-dom/client' in 搜了下原因是: 最佳回答: 您必须将React&ReactDOM Version升级到最新版本才能使用createRoot npm i react@latest react-dom@latest

vieyahn2017 commented 11 months ago

方案二, 改组件的package.json

change package.json of react-schema-render to my react version, then resolve it.

"peerDependencies": { "react": "^18.2.0" },

vieyahn2017 commented 11 months ago

方案三

这个问题在自己开发组件并且本地使用link调试的时候经常出现。 给个第二种解决方案: 在webpack.config.js中配置resolve.alias 属性为 { react: path.resolve('./node_modules/react') } 来保证项目本地启动时统一使用项目自身安装的react模块,而非第三方组件中的react模块。

webpack.config.js在哪里。。。 通过create-react-app创建的项目,webpack.config.js是在依赖包react-scripts中,可以通过npm run eject命令弹出。之后在根目录下的config目录中找到。

npm run eject命令 如果熟悉webpack的小伙伴,知道package.json中的配置会很多,而react脚手架中的package.json中,依赖为什么这么少。 这是因为像webpack,babel等等都是被creat react app封装到了react-scripts这个项目当中,包括基本启动命令 都是通过调用react-scripts这个依赖下面的命令进行启动的。 npm run eject,会将原本creat react app对webpack,babel等相关配置的封装弹射出来, 如果我们要将creat react app配置文件进行修改,现有目录下是没有地方修改的, 此时,我们就可以通过eject命令将原本被封装到脚手架当中的命令弹射出来,然后就可以在项目的目录下看到很多配置文件。 但这个操作是不可逆的,我们无法再通过其他方式将这些暴露出来的配置还原回去。