Invalid hook call on npm module

Viewed 4376

First of all link to the repo: https://github.com/vmarchesin/react-konami-code

You should be able to build it (make sure to uncomment the export on src/index.js) and link to your project. It is also possible to try it out using npm i -S react-konami-code@2.0.0-beta.0 and get the error for the hook. I removed the hook from 2.0.0-beta.1 because it's broken.

Description of the problem

I created a custom hook for my npm module and it doesn't work after being published or using it as a module. I suspect the issue is with how webpack bundles it but I can't solve it.

Steps taken

  • I made sure to declare react and react-dom as externals in my webpack config.
externals: [
  {
    react: {
      root: 'React',
      commonjs2: 'react',
      commonjs: 'react',
      amd: 'react',
    },
    'react-dom': {
      root: 'ReactDOM',
      commonjs2: 'react-dom',
      commonjs: 'react-dom',
      amd: 'react-dom',
    },
  },
],
  • Declared react and react-dom as peerDependencies in the package.json
"peerDependencies": {
  "react": "^16.13.1",
  "react-dom": "^16.13.1"
},
  • The hook is working. If I declare it and use it, it works. If I import it from the module (or use npm link) it doesn't. Here is the code for the hook:
import { useEffect, useState, useCallback } from 'react';

export default (action, {
  code = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65],
} = {}) => {
  const [input, setInput] = useState([]);

  const onKeyUp = useCallback(
    (e) => {
      const newInput = input;
      newInput.push(e.keyCode);
      newInput.splice(-code.length - 1, input.length - code.length);

      setInput(newInput);

      if (newInput.join('').includes(code.join(''))) {
        action();
      }
    },
    [input, setInput, code, action],
  );

  useEffect(() => {
    document.addEventListener('keyup', onKeyUp);
    return () => {
      document.removeEventListener('keyup', onKeyUp);
    };
  }, [onKeyUp]);
};
  • I am not calling the hook inside a class component.

I am using webpack@4.43.0 as well as babel@7 and babel-loader@8.1.0.

Here is how I export the index file for the webpack build:

import Konami from './Konami';

export default Konami; // disregard this, it works
export { default as useKonami } from './useKonami'; // this doesn't work

Here's a screenshot and transcript of the error:

Unhandled Runtime Error 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:

  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
3 Answers

I had the same issue, tried all the solutions and none of them worked.

The solution to this is, we need to configure Webpack to essentially ignore React as part of the production bundle along with peer dependency.

To do this, add below lines in webpack.config.js:

externals: {
   "react": "commonjs react",
   "react-dom": "commonjs react-dom",
}

I had the same error message while trying to import a react component from a npm package that I built. As the explanation of the error message suggests, this was propably a case of a duplicate React.

The important part concerning the error message is to bundle react and react-dom as external imports, so that the react of the parent project is being used, instead of using a second, that is a duplicate React.

I solved that by building my npm package with rollup.js, following this guide basically.

My resulting rollup.config.js:

import pkg from './package.json'
import babel from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    {
      file: pkg.main,
      format: 'cjs',
      exports: 'named',
      sourcemap: true,
      strict: false
    }
  ],
  plugins: [babel({ babelHelpers: 'bundled' })],
  external: ['react', 'react-dom']
}

You need the following dev-dependencies in your npm package (assuming you also have to transpile with babel):

npm i -D rollup babel-core @rollup/plugin-babel

You can bundle with rollup like this:

// add that to your package.json
"scripts": {
  "build": "rollup -c",
  "start": "rollup -c -w"  // for watching changes
},

With npm link it is even possible to get a hot-reload in the parent project for the packaged react component you are developing, without any npm publishing. Yay.

I see two issues here which might cause that error what you see on the console:

  1. Not importing React so breaking the Rules of Hooks - Only Call Hooks from React Functions.
  2. Also the issue can come from you don't have a named component here.

So I would include React as the following:

import React, { useEffect, useState, useCallback } from 'react';

Also you can try to export it as the following:

export default function ComponentName (action, {
  code = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65],
} = {}) {
   // your component code
}

Or if you want to still use arrow function then:

const ComponentName = (action, {
  code = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65],
} = {}) => {
   // your component code
}

export default ComponentName;

You can refer to this article:
Do not use anonymous functions to construct React functional components.

I hope this helps!

Related