Rashomon
Rashomon

Reputation: 6762

Solve having more than one copy of React in the same app

I'm developing a React module locally. For that, I'm linking my module using npm link. The module is imported successfully but hooks are failing inside the module. It's throwing 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 See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

Checking the suggestions at React docs, I can confirm my app is using duplicate versions of React since the following code returns false:

// node_modules/mymodule/src/index.js
export { default as ReactFromModule } from 'react'
// src/index.js
import React from 'react'
import { ReactFromModule } from 'mymodule'
console.log(React === ReactFromModule) //false

This issue is full of suggestions but they are confusing. How can I solve it?

Note: Im not breaking rules of hooks, the error appears only when importing the module from an application.

Upvotes: 43

Views: 68798

Answers (7)

Code Forge Temple
Code Forge Temple

Reputation: 11

What worked for me was:

  1. in the module/library I was developing, I had to:
  • set in the webpack.config.js:
    externals: {
        react: "react",
        "react-dom": "react-dom",
    },

see https://github.com/code-forge-temple/circuit-sketcher-core/blob/main/webpack.config.js

  • and in the package.json:
    "peerDependencies": {
        "react": ">=18.0.0",
        "react-dom": ">=18.0.0"
    },

see https://github.com/code-forge-temple/circuit-sketcher-core/blob/main/package.json

  1. in the project using the created module from (1), I had to:
  • set in the webpack.config.js:
    alias: {
      react: path.resolve(__dirname, "node_modules/react"),
      "react-dom": path.resolve(__dirname, "node_modules/react-dom"),
    },

see https://github.com/code-forge-temple/circuit-sketcher-obsidian-plugin/blob/main/webpack.config.js

  • add in the package.json:
    "dependencies": {
        "circuit-sketcher-core": "github:code-forge-temple/circuit-sketcher-core",
        "react": "^19.0.0",
        "react-dom": "^19.0.0"
    },

see https://github.com/code-forge-temple/circuit-sketcher-obsidian-plugin/blob/main/package.json

  1. in the project (with Vite) using the created module from (1), I had to:
  • update the vite.config.ts with the following:
    resolve: {
        dedupe: ["react", "react-dom"],
    },

see https://github.com/code-forge-temple/circuit-sketcher-app/blob/main/vite.config.ts

  • add in the package.json:
  "dependencies": {
    "circuit-sketcher-core": "github:code-forge-temple/circuit-sketcher-core",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },

see https://github.com/code-forge-temple/circuit-sketcher-app/blob/main/package.json

Upvotes: 1

Henry Woody
Henry Woody

Reputation: 15662

Another approach that works if you're using Vite/Vitest is to use the resolve.dedupe option.

From the docs:

If you have duplicated copies of the same dependency in your app (likely due to hoisting or linked packages in monorepos), use this option to force Vite to always resolve listed dependencies to the same copy (from project root).

Here's an example:

import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import react from "@vitejs/plugin-react";


export default defineConfig({
    plugins: [react(), tsconfigPaths()],
    resolve: {
        dedupe: ["react", "react-dom"],
    }
});

In my particular case the issue was caused by @testing-library/react, so I've included that in my dedupe list.

Upvotes: 5

Rashomon
Rashomon

Reputation: 6762

In the module you are developing, add the conflicting packages to peerDependencies (and remove them from dependencies or devDependencies):

  // package.json
  "peerDependencies": {
    "react": "16.13.1",
    "react-dom": "16.13.1"
  },

Execute npm install in your module.

Now add react and react-dom to the webpack configuration of your module as externals. These packages shouldnt be included in the bundle of the module (the app that uses the module will provide them):

// webpack.config.js
module.exports = {
    /*
    rest of config...
    */
    output: {
        filename: "index.js",
        pathinfo: false,
        libraryTarget: 'umd', // In my case, I use libraryTarget as 'umd'. Not sure if relevant
    },
    externals: {
        // Use external version of React
        "react": {
            "commonjs": "react",
            "commonjs2": "react",
            "amd": "react",
            "root": "React"
        },
        "react-dom": {
            "commonjs": "react-dom",
            "commonjs2": "react-dom",
            "amd": "react-dom",
            "root": "ReactDOM"
        }
    },
};

Then, after building your module, in your application you can check that both versions are now the same:

// node_modules/mymodule/src/index.js
export { default as ReactFromModule } from 'react'
// src/index.js
import React from 'react'
import { ReactFromModule } from 'mymodule'
console.log(React === ReactFromModule) // true :)

Upvotes: 39

varun dhiman
varun dhiman

Reputation: 23

In my case I was also missing import React from 'react' from couple of files. check this

Upvotes: -3

Matthew Buckett
Matthew Buckett

Reputation: 4371

I was attempting to use the peerDependencies and removal of the devDependencies and it was failing.

It turned out I had a node_modules folder in one of the parent folders of the library I was working on and the duplicate version of React was being loaded from there instead of the tool that was trying to use the React library.

Rather than editing the devDependencies to remove react I just wrote a small script to delete anything that's in the peerDependencies from the node_modules folder.

npm view --json=true . peerDependencies | jq -r 'keys | .[] |  @text' | while read dep; do  rm -r ./node_modules/${dep} && echo Removed ${dep}; done

Upvotes: -1

L. Pier Roberto
L. Pier Roberto

Reputation: 750

Adding react and react-dom as peerDependencies in the package.json didn't work for me.

I had to add an alias to the webpack configuration file:

// webpack.config.js
resolve: {
  alias: {
    react: path.resolve('./node_modules/react'),
}

Upvotes: 24

Vincent La
Vincent La

Reputation: 504

In response to another comment, merely moving React to peerDependencies does not adequately resolve the issue in all cases. I would reply to that comment directly, but StackOverflow requires more reputation to respond to wrong answers than it does to post them.

I have a shared React component module built using Webpack and have run into the same issue. I've outlined one possible fix in this comment below which requires modifying peerDependencies and using npm link in a fashion similar to the answer shared by mtkopone. https://github.com/facebook/react/issues/13991#issuecomment-841509933

My solution is a bit hacky and I wouldn't recommend it for long-term use. If you are using Webpack (which you tagged this question as), this article may detail a more permanent solution (https://medium.com/codex/duplicate-copy-of-react-errors-when-using-npm-link-e5011de0995d). I haven't tried it yet, but the author seems to have tried all the (incorrect) solutions out there and is also running into the hooks issue while trying to build shared component libraries.

The author of that article is trying to debug a Create-React-App app. While CRA uses webpack under the hood, you can't access the webpack.config directly, so the author has to perform some workarounds to do so. If you aren't using CRA, but just plain Webpack, then you could consider using the resolve.alias section of webpack.config to ensure there are no duplicate copies of React (see: https://blog.maximeheckel.com/posts/duplicate-dependencies-npm-link/)

Upvotes: 5

Related Questions