How-To: Fix 'more than one copy of react in the same app'
25th March 2021
A look at a common React error sometimes seen when developing reusable components
Here's the thing. You're building a reusable React component in separate npm package, you're testing out that component in a local app, you fire up the app and then...
Boom! 💥
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](https://reactjs.org/link/invalid-hook-call) for tips about how to debug and fix this problem.
As the text suggests, this error can appear for several reasons. This article examines the situation when developing a standalone React component, looking at the cause and also some solutions.
The problem
When developing a standalone React component it often makes sense to try out the component in a local app. In the example below, app contains a standard React app which pulls in an external component defined in the sibling folder component:
-
app
-
node_modules
- component ↪ ../../component (symlink)
- react
- index.js
- package.json
-
node_modules
-
component
-
node_modules
- react
- index.js
- package.json
-
node_modules
Note above that app/node_modules/component is symlinked to the top level component folder, more on this later.
Also note that the react library appears twice in this folder structure:
- app/node_modules/react - by definition React must be a dependency of the app
- component/node_modules/react - React might be a devDependency of the component and devDependencies are present while developing (a correctly setup component should have React in the peerDependencies section of package.json and probably also in the devDependencies section but never in the dependencies section).
So when the app is built it ends up containing two copies of React, hence the third part of React's error:
You might have more than one copy of React in the same app
But what can be done about this? 🤔
Solution 1: Fix with config
The webpack solution
If using webpack then the solution should be just a case of setting up a resolve alias in the app's webpack.config.js file:
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
'react': path.resolve(__dirname, 'node_modules/react'),
}
},
};
This instructs webpack to always use the copy of React found in the app/node_modules folder, so only one copy of React gets included in the bundle.
The create-react-app solution
For an app using create-react-app things are a little different. create-react-app uses webpack 'under the hood' but it's a bit harder to change the webpack config. One approach is to eject and then make the change to the ejected webpack.config.js (as outlined above). Another approach is to install react-app-rewired and use the following config:
config-overrides.js
const path = require('path');
module.exports = {
webpack(config) {
config.resolve.alias['react']
= path.resolve(__dirname, 'node_modules/react');
return config;
}
};
Solution 2: Move to a monorepo with workspaces
An alternative approach is to move to a monorepo with workspaces. This means that the app and the component become part of the same repo - this may not make sense for all projects but in many situations it could be beneficial.
Workspace management is a feature of both yarn and more recently npm v7.
Here's the same project as a monorepo with workspaces:
-
monorepo
-
node_modules
- component ↪ ../packages/component (symlink)
- react
- packages
-
app
- index.js
- package.json
-
component
- index.js
- package.json
-
app
- package.json
-
node_modules
app and component are now nested inside a packages folder and component is now symlinked inside the top level node_modules folder. react is now only present once in the top level node_modules folder, that's because the management of npm packages becomes a bit more intelligent when using workspaces, common dependencies like React are 'hoisted' to the root. Having just a single copy of React means just one copy will be used in the app bundle, this should eliminate the You might have more than one copy of React in the same app
error.
The workspaces are defined by a root level package.json with the following contents:
{
"private": true,
"workspaces": [
"packages/**"
],
}
Using workspaces offers many other advantages too, particularly if there are multiple components - see yarn workspaces for more details.
A quick note about symlinking
As mentioned in the first example, component was symlinked into the app/node_modules folder. This symlinking can be managed by npm - simply specify the local filesystem path to the component inside the app's package.json dependencies
section:
{
"dependencies": {
"component": "file:../component",
"react": "^17.0.1",
}
...
}
It can also be setup using this npm command:
npm install component@file:../component
Symlinking the component folder inside the app's node_modules can be very useful while developing; any changes to the component will be picked up immediately inside the app without having to reinstall.
See package.json local paths for more details.
... and a quick note about package name security
This example uses the rather arbitrary name component
- when choosing a name for a non-public package be aware of the possibility of npm substitution attacks
Wrapping up
So the simple solution to the You might have more than one copy of React in the same app
error is to add rules into the bundler's config (webpack discussed here but similar approaches are available for other bundlers such as Snowpack, rollup.js or Parcel).
However it could be that the error is trying to tell us something, perhaps it's time to consider a monorepo? 🧐
Thanks for reading!