3

I have an NPM package I am working on which has a dependency of react. I then have a test app which has react installed as a dependency. When I import my npm package into the test app, I get the following 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

Running npm ls react in my test app suggests I might have a duplicate of react:

test-app@0.1.0 
├─┬ @package-name/react@1.0.0 -> ./../package-name-react
│ ├─┬ react-dom@17.0.2
│ │ └── react@17.0.2 deduped
│ └── react@17.0.2 // <----------------
├─┬ next@12.1.0
│ ├── react@17.0.2 deduped
│ ├─┬ styled-jsx@5.0.0
│ │ └── react@17.0.2 deduped
│ └─┬ use-subscription@1.5.1
│   └── react@17.0.2 deduped
├─┬ react-dom@17.0.2
│ └── react@17.0.2 deduped
└── react@17.0.2 // <----------------

My package.json for my npm package looks like this:

{
  "name": "@package-name/react",
  "version": "1.0.0",
  "description": "",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "files": [
    "dist"
  ],
  "types": "dist/index.d.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "clean": "rimraf dist",
    "build": "npm run clean && rollup -c"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^20.0.0",
    "@rollup/plugin-node-resolve": "^13.0.4",
    "@rollup/plugin-typescript": "^8.2.5",
    "rimraf": "^3.0.2",
    "rollup": "^2.56.2",
    "rollup-plugin-dts": "^3.0.2",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-postcss": "^4.0.1",
    "rollup-plugin-terser": "^7.0.2",
    "typescript": "^4.3.5"
  },
  "dependencies": {
    "socket.io-client": "^4.4.1"
  },
  "peerDependencies": {
    "react": "17.0.2",
    "react-dom": "17.0.2"
  }
}

When I remove react and react-dom from peerDependencies, the error goes away but causes other issues. It's almost like peerDependencies are being installed and rolled up in my package.

My component in my package is very simple at this stage and is like so:

const MyComponent  = ({ 
  children 
}) => {
  const [myValue, setValue] = useState(false);

  useEffect(() => {
    setFlagValue(true)
  }, []);

  return (
    <>
      {children}
    </>
  )
};

I am then consuming this package in my test app like so:

import { MyComponent } from "package-name/react";

const MyApp = () => {
  <MyComponent>
    <div>Hello world</div>
  </MyComponent>
}

Rollup config:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import external from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import dts from 'rollup-plugin-dts';

const packageJson = require('./package.json');

export default [
    {
        input: 'src/index.ts',
        output: [
            {
                file: packageJson.main,
                format: 'cjs',
                sourcemap: true,
                name: 'react-ts-lib'
            },
            {
                file: packageJson.module,
                format: 'esm',
                sourcemap: true
            }
        ],
        plugins: [
            external(),
            resolve(),
            commonjs(),
            typescript({ tsconfig: './tsconfig.json' }),
            postcss(),
            terser()
        ],
    },
    {
        input: 'dist/esm/types/index.d.ts',
        output: [{ file: 'dist/index.d.ts', format: "esm" }],
        external: [/\.css$/],
        plugins: [dts()],
    },
]
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Stretch0
  • 8,362
  • 13
  • 71
  • 133
  • Using a version range for peer dependencies is best, since there's more chance that it fits the consuming project's dependencies. – Emile Bergeron Mar 08 '22 at 18:24
  • That said, how you are using the code is likely the issue. Could you provide a [mcve]? – Emile Bergeron Mar 08 '22 at 18:26
  • 1
    Thanks for your response. I have updated peerDeps too use range like so: `"react": "^16.0.2",` but still the same issues. I agree that it's good practice but not causing the issue here as I am running the same react version on both. – Stretch0 Mar 08 '22 at 18:33
  • 1
    I have added a code snippet to show how my very simple component in my package looks like and how it is being consumed by my test app. As you can see, there is no logic happening yet and is purely justta state update in a useEffect. – Stretch0 Mar 08 '22 at 18:34
  • I'd like to highlight the results of `npm ls react` are showing a duplicate of the react package is installed. I suspect the issue is more to do with peerDeps being installed in my dist or rollup config causing it to bundle peerDeps? – Stretch0 Mar 08 '22 at 18:35
  • It's a possibility that React gets bundled with your lib, which duplicate the dependency within your other project. It's hard to tell since we don't have your bundling configuration though. – Emile Bergeron Mar 08 '22 at 18:56
  • Maybe just adding your `rollup.config.js` file to the question would be enough. Something like a missing `external` config could create this issue. – Emile Bergeron Mar 08 '22 at 19:00
  • 1
    I have added my rollup config. It should be excluding deps and checking my `./dist` file, I can't see any node modules in there so i think it is working as expected – Stretch0 Mar 08 '22 at 19:05
  • 1
    I don't see any obvious issue right now, I'd have to try it to debug more in depth, but I unfortunately can't right now. If you were able to repro in something like a code-sandbox (if even possible with that setup), it would help debugging. But don't feel pressure to do so, maybe someone more knowledgeable with rollup and peer deps will come around. – Emile Bergeron Mar 08 '22 at 20:42
  • 1
    @EmileBergeron I have created this simple repo with the issue here to make it easy to replicate https://github.com/stretch0/test-app – Stretch0 Mar 09 '22 at 16:07
  • I've created an issue on that repo as it seems some code is missing in a directory that appears to be a symlink. – Emile Bergeron Mar 09 '22 at 16:47
  • Have updated, let me know if you have any more issues – Stretch0 Mar 09 '22 at 18:56

1 Answers1

2

It was not clear from the question description, but looking at the repo, I see that the package is installed locally.

"dependencies": {
  "next": "12.1.0",
  "react": "17.0.2",
  "react-dom": "17.0.2",
  "react-ts-lib": "file:../react-ts-lib"
},

Which means that the lib code still resolves react using its own node_modules (installed locally) rather than the app dependencies.

One way to fix this issue could be to setup the lib project with something like create-react-library, which addresses this problem explicitly:

If you use react-hooks in your project, when you debug your example you may run into an exception Invalid Hook Call Warning. This issue explains the reason, your lib and example use a different instance, one solution is rewrite the react path in your example [app] package.json to 'file:../node_modules/react' or 'link:../node_modules/react'.

Note that in your case, you could simply change the path of react in the lib's devDependencies to point to the app's react:

  "devDependencies": {
    "@rollup/plugin-commonjs": "^20.0.0",
    "@rollup/plugin-node-resolve": "^13.0.4",
    "@rollup/plugin-typescript": "^8.2.5",
    "@types/react": "^17.0.18",
    "react": "file:../my-app/node_modules/react",

Or the other way around, depending on what makes the most sense.


There are also other ways to do local development of a React library:

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • 1
    Thanks so much. You are a legend. Such a good answer with so many options as well as going out of your way to pull down the local setup of the sandbox env I setup. Thanks so much! – Stretch0 Mar 10 '22 at 10:07
  • 1
    It's a pleasure! It's been awhile since I've worked on a React lib setup and we were using a yarn monorepo which makes this way easier to manage. – Emile Bergeron Mar 10 '22 at 15:15