I have a fairly standard lerna monorepo that will look like this:
packages/
main/ - This is the main deployable application, it depends on both dep and react-dep
dep/ - Just some pure functions, no problems here
react-dep/ - A design system built with react, this is where we have problems.
So a really common problem that happens, is as soon as you start using hooks in your dependency library, you get this message:
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/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.
Which is because there are two version of React in your app, one from the main application, and one from the dependency.
Now - a common solution, that I've used, and works, is to declare react
and any other shared/peer dependencies as externals
in your webpack configuration. Eg, as suggested here. or look at this Github issues thread from react.
However, I'm not a fan of this solution, firstly, what if I'm not using webpack, and secondly I shouldn't have to manually keep track of which dependencies I need to mark as external.
I think what should work is:
In react-dep
I declare react
in both devDependencies
and peerDependencies
. The reason I put it in devDependencies
is because my dependency library is likely using storybook or similar to develop the components, so I do need react around in development.
I think that this should work if I'm publishing react-dep
to npm and consuming the compiled code from npm in main
, because only dependencies
are going to be fetched.
However, I think due to the lerna symlinking, what happens in this case is that the dev dependency is still around and we get this error.
Is there a way to solve this issue for a lerna monorepo?
Here's github repo that demonstrates this issue: https://github.com/dwjohnston/lerna-react-monorepo
Peer Dependencies are used to specify that our package is compatible with a specific version of an npm package. Good examples are Angular and React. To add a Peer Dependency you actually need to manually modify your package.json file.
Solution 1: Ignore the peerDependencies The easiest way to fix the issue is to pass an additional parameter –legacy-peer-deps to npm install. The --legacy-peer-deps tells the npm to ignore the peer dependencies and continue the installation of the package.
Ask user to install a dependency your module needs to work without specifying a version in particular. Prevents having multiple version of a same module in user's app node_modules. Reduce javascript files size to load on browser side particularly useful for mobile users.
In both cases, when you install a package, its dependencies and devDependencies are automatically installed by npm. peerDependencies are different. They are not automatically installed.
As I see this problem potentially can be solved using lerna
, npm
, yarn
or webpack
.
I want to propose one more webpack
solution there, opened a pr to your repo. If a webpack
solution is a bad fit for you - just ignore this answer.
It is a little better than externals
mechanism, because it will track overlapping peer dependencies automatically.
module.exports = function(config) {
config.plugins.push(
new NormalModuleReplacementPlugin(re, function(resource) {
// first, remove babel and other loaders paths
const requestWithoutLoaderMeta = resource.request.split('!');
const requestPath = requestWithoutLoaderMeta.length && requestWithoutLoaderMeta[requestWithoutLoaderMeta.length - 1];
if (requestPath) {
// looking for a dependency and package names
const packagesPath = resolve(__dirname, '../') + '/';
const requestPathRel = requestPath.replace(packagesPath, '');
const [packageName, _, depName] = requestPathRel.split('/');
// if the main package has this dependency already - just use it
if (dependencies[packageName]) {
console.log('\x1b[35m%s\x1b[0m', `[REPLACEMENT]: using dependency <${depName}> from package [main] instead of [${packageName}]`);
resource.request = resource.request.replace(`${packageName}/node_modules/${depName}`, `main/node_modules/${depName}`)
}
}
})
);
return config;
}
This code will resolve your peer dependencies from main
package using webpack.NormalModuleReplacementPlugin
.
Note about webpack: since all three frontend kings use it in their CLI (angular, react, vue) I think you can use it easily and safely for such customizations.
I'm happy to hear about alternative technologies (eg. yarn) to solve this problem.
Give @nrwl/nx a try for your next monorepo instead of lerna
.
The main difference there is that nrwl
projects usually have ONE package.json
only (using Google experience), so you need to install the dependencies once for all packages and will not face the problem you described.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With