I recently encountered an interesting problem at work. Document the process from problem discovery to solution.

The questions include:

  • Source code logic

  • Package. The json configuration

the

A requirement requires the introduction of a third-party component library.

React generates an error when introducing A function from A component library:

“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…

As you know from the React documentation, this is due to the incorrect use of Hooks.

According to the official website, there are three possible reasons for errors:

  1. ReactandReactDOMVersion mismatch

ReactDOM above V16.8 is required to support Hooks.

Our project uses V17.0.2, which does not belong to this reason.

  1. To break theHooksThe rules of

Hooks can only be called on top of function components or custom Hooks.

UseRef = useRef; useRef = useRef;

function A() {
  // ...
  var xxxRef = useRef(null);
  // ...
}
Copy the code

It doesn’t belong to this cause.

  1. repeatedReact

React document:

In order for the Hook to work properly, the React dependencies in your application code and the React dependencies used inside the React-DOM package must be resolved into the same module.

If these React dependencies resolve into two different exported objects, you’ll see this warning. This can happen if you accidentally introduce two copies of the React package.

It’s a tricky read. Looks like this is the most likely suspect.

Location problem

A breakpoint is placed on the useRef that reported the error. It is from:

http://localhost:8081/Users/ project directory/node_modules / ` component library/node_modules/react/CJS/react development. Js `

Set breakpoints elsewhere in the project where Hooks are called but no errors are reported, and find resources from:

http://localhost:8081/Users/ project directory/node_modules / ` react/CJS/react. Development. Js `

The error is reported by useRef and other Hooks in the project that reference different react.development.js.

Install react and react-dom as dependencies.

"dependencies": {
  "react": "^ 16.13.1"."@babel/runtime-corejs3": "^ 7.11.2." "."react-dom": "^ 16.13.1"
},
Copy the code

This creates both dependencies under node_modules in the component library directory.

As a component library, this is obviously inappropriate.

A temporary solution

It is best to refer to these two dependencies as peerDependencies, which are external dependencies.

This way, when we import the component library, the component library will use react and React-DOM in our project instead of installing a copy ourselves.

But I don’t have access to this component library, so I have to work on my own projects.

A configuration item, Resolutions, is provided in the package.json document to temporarily resolve this problem.

Resolutions allows you to override a version of the package that is nested referenced in the project node_modules.

Make the following changes in our project’s package.json:

/ / project package. Json
{
  // ...
  "resolutions": {
    "react": "17.0.2"."react-dom": "17.0.2"
  },
  // ...
}
Copy the code

Thus, both dependencies used in your project will use the versions specified in the Resolutions.

React and react-dom in both the component library and our project code point to the same file.

Now the problem is temporarily solved, but what is the cause of the problem?

Let’s dig deep into the Hooks source code to find out.

Deep source

First, let’s consider two questions:

This error is reported when we call other Hooks from within one Hooks.

For example, the following code will return an error:

function App() {

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

  // ...
}
Copy the code

Hooks are just functions, how does he sense that he is executing inside another Hooks?

In the example above, how does useRef sense that it is executing in the useEffect callback?

ComponentDidMount and componentDidUpdate are the life cycle functions for classComponent to distinguish between mounting and Updating.

Which function do you use to distinguish between mount and update?

Clearly, there is a mechanism within the Hooks source code to be aware of the context in which the current execution is taking place.

get

In the browser environment, we reference the React and reactDOM packages.

There is a variable ReactCurrentDispatcher in the react package code.

His current parameter points to the Hooks context currently in use:

var ReactCurrentDispatcher = {
  / * * *@internal
   * @type {ReactComponent}* /
  current: null
};
Copy the code

At the same time, in the process of program is running, in reactDOM ReactCurrentDispatcher. The current will differ according to the current context environment to reference.

Such as:

var HooksDispatcherOnMountInDEV = {
  useState: function() { // ... },
  useEffect: function() { // ... },
  useRef: function() { // ... },
  // ...
}
var HooksDispatcherOnUpdateInDEV = {
  useState: function() { // ... },
  useEffect: function() { // ... },
  useRef: function() { // ... },
  // ...
}
// ...
Copy the code

When in the DEV environment mount ReactCurrentDispatcher. Current will point HooksDispatcherOnMountInDEV.

When in the DEV environment update ReactCurrentDispatcher. Current will point HooksDispatcherOnUpdateInDEV.

Let’s look at the definition of useRef:

function useRef(initialValue) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
Copy the code

The internal call is dispatcher.useref.

The dispatcher is ReactCurrentDispatcher. Current.

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

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

As you can see, the opening error was thrown when dispatcher is null

That is why Hooks distinguish mount from Update.

By the same token, the DEV environment, when a Hooks at execution time, ReactCurrentDispatcher. The current will point reference – InvalidNestedHooksDispatcherOnUpdateInDEV.

Hooks called in this case, such as useRef:

var InvalidNestedHooksDispatcherOnUpdateInDEV = {
  // ...
  useRef: function (initialValue) {
    currentHookNameInDev = 'useRef';
    warnInvalidHookAccess();
    updateHookTypesDev();
    return updateRef();
  },
  // ...
}
Copy the code

Error: warnInvalidHookAccess executes in other Hooks.

The truth

At this point, we finally know the essential cause of the problem mentioned in the beginning:

  • Because the component library uses dependencies instead of peerDependencies, the react and reactDOM referenced in the component library are files under node_modules.

  • The react and reactDOM used in the project are files in the node_modules directory.

  • React in the component library and react in the project directory initialize ReactCurrentDispatcher at runtime, respectively

  • The two ReactCurrentDispatcher depend on the reactDOM of the corresponding directory

  • We are in the project implementation project directory reactDOM reactDOM. Render method, he will change the project directory as the program runs the react ReactCurrentDispatcher under the package. The current point

  • In the component library ReactCurrentDispatcher. Current is always null

  • When calling the Hooks in the component library, because ReactCurrentDispatcher. Current is always null result in an error

conclusion

By analyzing this problem, you have deepened your understanding of package.json and Hooks source code.

Are you inspired by the context-aware implementation of Hooks?