Custom React Component Library - jest 'cannot find module react'- testing-library, rollup - javascript

I'm building a custom react component library to share with other applications. I'm using rollup and following this blog and a few others: https://dev.to/alexeagleson/how-to-create-and-publish-a-react-component-library-2oe
The relevant snippet of my rollup config is as follows:
export default [
{
input: "src/index.ts",
output: [ { file: packageJson.main, format: "esm", sourcemap: true } ],
plugins: [ peerDepsExternal(), resolve(), terser() ],
external: ["react", "react-dom"]
}
]
In my package.json I've moved react and react-dom from 'devDependencies' to 'peerDependencies'. My abbreviated package.json:
{
"name": "custom components",
"version": "1",
"devDependencies": {
...stuff, (but not react/react-dom)
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"main": "dist/esm/index.js",
"files": ["dist"]
}
Then using npm link I've imported my component library into another app which is using CRA and react-testing-library.
So far this works. I'm able to render my common components as expected.
However it seems like moving react/react-dom out of devDependencies is making my jest tests fail in both projects. In both cases jest cannot find react. The Jest output in my CRA that imports the components is as follows:
Test suite failed to run
Cannot find module 'react' from 'index.js' //<-- this is the index of the component library
However, Jest was able to find:
.../MyComponent.tsx
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['web.js',...etc] (defaults)
And when I run my tests in my common component library:
Test suite failed to run
Cannot find module 'react' from 'MyComponent'
import React from 'react';
^
I'm not sure how exactly to get around this. If I move react/react-dom back to devDependencies, the tests will run successfully, but then any component using react state will fail to render due to multiple copies of React in the project.
Could this be an issue because I'm using npm link as opposed to actually publishing the application to npm and installing or is this an issue with my rollup config/jest config/package.json? Any help would be appreciated.

react and react-dom should be included in both devDependencies and peerDependencies of your library.
Including them in devDependencies makes them available when developing (and when running tests), but they won't be included in the library when you bundle them with Rollup (which is what you want).
Including them in peerDependencies tells any consuming applications "you must have these dependencies at the specified version range," but again, Rollup doesn't include them in the library bundle.
If you only include them in peerDependencies, they aren't installed when developing the library or running tests, although this might no longer be true if you're using npm v7.
You can run into the problem of having multiple versions of React in your project if your app includes them at a different version than your library, but if both the app and the library have the same version range you shouldn't have that problem.

Related

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.
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 :)
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'),
}
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/)
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
In my case I was also missing import React from 'react' from couple of files.
check this

One react app imported into another react app?

Is it possible to import one React app(built using create-react-app) into another completely different React app. We all have imported components in our SPA, but is it possible to import a different app entirely? If so how?
Furthermore, both these react apps MIGHT share a few similar dependencies/files/libraries.. such as bootstrap/css/Redux stores/etc. Also, with a possibility of a few common reducers. These common reducers, might need to interact/listen to actions in-between these two react app.
Lets say we have:
ReactDOM.render(<Provider store={store}><MainApp /></Provider>, document.getElementById('root'));
Could i add another app like this(that was NOT built in this), and target another nod in the dom???
ReactDOM.render(<Provider store={store}><ExternalAPP /></Provider>, document.getElementById('root22'));
Has this ever been done before? React compresses all our components into "chunks" which are basically js files.
Thank you, for any tips/suggestions/hints
You can use npm pack command.
It creates a tarball (.tgz) file from your current app. Then move this file your other app folder then run:
npm install app1 (assuming app1 is your first app name).
After it is installed, you can see your app1 files inside the node_modules/App1/. Now your can import the files or components you want and use it in other apps.
Hope this helps.
Also Checkout this: https://docs.npmjs.com/cli-commands/pack.html
One way that worked for me is to bring the CRA to Github and npm install it as a dependency. I highly encourage you to check out this resource which explains in detail how to prepare a React component for this process. Follow the steps in the linked tutorial up to #3, then bring everything (including the /dist folder) to Github. Then, do npm install org_name/repo_nameand install it as a dependency in your second React app. Then, to import a specific component, you can do something like import { Button } from 'repo_name/dist/index' or reference whatever file you used to export your components.
In case the link doesn't work, here are the steps in the article:
Create a folder called lib that stores all the components you want to bring to the other react app. Also define a file called index.js in this folder that exports these components.
Create a repo for the components on Github
Install Babel and build the dist folder.(npm install --save-dev #babel/core #babel/cli #babel/preset-env and npm install -save #babel/polyfill)
Add the following config file to the top-level folder of your project:
{
"presets": [
[
"#babel/env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
],
"#babel/preset-react"
]
}
In package.json, replace the build script with the following: "build": "rm -rf dist && NODE_ENV=production babel src/lib --out-dir dist --copy-files";
Run the command npm run build

vue cli build with target lib: "require is not defined" when component is imported

I'm trying to export a Vue component as a package, and using vue cli to build the dist. I intend to publish it on npm, but I'm currently using a symbolic link for testing purpose. However even with a simple hello-world project I can't make a valid package.
I created a project:
vue create hello-world
Then I modified the package.json:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --target lib --name vue-hello-world ./src/components/HelloWorld.vue",
"lint": "vue-cli-service lint"
},
"main": "./dist/vue-hello-world.common.js",
Then I call
npm run build
and it compiles without error.
Then I make an import in a vue component in another project (I used a symbolic link in node_modules):
import HelloWorld from "hello-world";
On page render I get the following error:
[Vue warn]: Failed to resolve async component: function MediaViewerPdf() {
return Promise.all(/*! import() */[__webpack_require__.e(62), __webpack_require__.e(46)]).then(__webpack_require__.bind(null, /*! ./viewers/MediaViewerPdf.vue */ "./vuejs/components/mediaViewer/viewers/MediaViewerPdf.vue"));
}
Reason: ReferenceError: require is not defined
Any idea what's happening?
Remarks:
using vue inspect, I checked that in webpack config that:
target: "web"
I already set resolve.symlinks at false on the importing project.
EDIT: I have confirmed that it doesn't come from the symbolic link, I have exactly the same error with package directly on node_modules.
Repo with whole code: https://github.com/louis-sanna/vue-hello-world
So I asked the question on the vue-cli repo and I got the solution! https://github.com/vuejs/vue-cli/issues/4245
Turns out NODE_ENV was already set at development in my shell, so it was the mode used to make the build.
Just need to set the mode explicitly and it works:
vue-cli-service build --target lib --name vue-hello-world ./src/components/HelloWorld.vue --mode production
You may need to add it to vue.config.js:
config
.mode("production")
This happens due to the fact that Vue CLI Webpack setup by default does not import commonjs modules, as described in your "main" field in package.json. So the problem is with the project that attempts import, not with the project that exports the component.
There are two ways to attempt to solve this problem.
From the importing project side
You can remedy this by installing babel plugins for the project that imports your components and setting babel.config.js
module.exports = {
presets: [
'#vue/app'
],
plugins: [
'#babel/plugin-transform-modules-commonjs', // leave to import .common.js files
'#babel/plugin-transform-modules-umd' // leave to import .umd.js files
]
}
But doing this alone will not be sufficient: you also will need to import CSS that is generated with the library by adding this in your entry file
import 'hello-world/dist/vue-hello-world.css';
Note that I have only tested this using yarn link, but I have confidence that this will work with an imported separate npm module just fine.
From the library side
The intent (I suppose) of the original question - how do I bundle a library so my users don't have to do this little dance each time they want to use it?
Option 1: don't bundle it - provide .vue files and sources. Just include everything in 'src' directory of your module, write a readme with explanation and be done with it. Let the importing project figure the compilation and bundling out.
Option 2: use rollup with Vue plugin to roll components into bundle. There is an example on how to do that. In that example you can see that your project will be able to import .esm build
https://github.com/vuejs/rollup-plugin-vue/tree/master/cookbook/library
Not sure how you are creating the symbolic link, but you should use npm link for that. If you are still having problems (like I did myself) I would suggest you try npm-link-better:
npm install -g npm-link-better
cd vue-hello-world
npm link
cd ../hello-world
nlc -w vue-hello-world
For building component libraries, I suggest you have a look at vue-cli-plugin-component. This plugin already sets up the vue-cli project pretty well.

Import custom component in React with react-select and crash with Error: Minified React error #188

I develop a custom library of components in React(16.4) and Webpack 4. Config was eject and import to other project with git+ssh://git#bitbucket.org.... on package.json.
Only one component doesn't work when I used this lib in other project, it's a select input develop with react-select (Jed Watson) and it crashes on click.
All app crashes and I have this error:
Uncaught Error: Minified React error #188; visit https://reactjs.org/docs/error-decoder.html?invariant=188 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
Message from link in error above:
Unable to find node on an unmounted component.
I now my problem is with my webpack config but I don't find any solutions to fix this.
Last hope with community...
I had a similar situation - I am developing a custom component library and importing it into an external project. I had issues with react-bootstrap, react-transition-group, and other external libraries that dynamically mount/unmount DOM elements in production; minified react error #188 would crash production builds. Here's how I solved it in our webpack config under module.exports (both dev and prod configs):
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
"react-native": "react-native-web",
react: path.resolve("./node_modules/react"),
"react-dom": path.resolve("./node_modules/react-dom"),
"react-transition-group": path.resolve(
"./node_modules/react-transition-group"
)
}
This should override any other instances of these particular libraries in all dependencies; anytime they're imported/used, they're forced to use the version that my project dictates in the package.json. Please keep in mind that I'm using Webpack 3, so the fix for Webpack 4 may be different.
Thank you Abi.
I've just resolve it with other way. I add all library in "externals" in my webpack config like this :
externals: [
'react-select': {
root: 'ReactSelect',
commonjs2: 'react-select',
commonjs: 'react-select',
amd: 'react-select',
},
]

How Prevent Multiple Copies Of React from Loading?

In my previous Meteor app, using browserify, and React, all was working until I switched to meteor webpack.
I use react-select in my Meteor apps and it worked great but with browserify I could prevent multiple copies of react from loading which prevents this error I'm now having:
Error: Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. You might be adding a ref to a component that was not created inside a component's `render` method, or you have multiple copies of React loaded (details: https://fb.me/react-refs-must-have-owner).
My package.json look this:
...
"dependencies": {
"classnames": "^2.1.3",
"lodash": "^3.10.0",
"react": "^0.14.6",
"react-dom": "^0.14.6",
"react-mixin": "^2.0.1",
"react-select": "^1.0.0-beta8"
},
...
Is there a configuration in webpack I could use something call externals? Not fully sure what that means but a comment said to use:
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
Since you use webpack, you can add an alias for loading react, like this:
// In webpack.config.js
resolve: {
alias: {
react: path.resolve('node_modules/react'),
},
},
This prevented the addComponentAsRefTo(...) error and made our build succeed again. However, for some reason, the testing build failed only on our CI environment as it could not resolve the node_modules/react path. I think it's unlikely that you will encounter this particular problem, though.
Something that worked for me was:
Uninstall all globally installed packages related to react (create-react-app, react-native, react and so on)
then: rm -rf node_modules
then: use npm install instead of yarn install
considering an App created with crate-react-app and ejected
In my case, I was building a separate npm module, then including that as a library in another project locally using npm link ../some-library. One of the modules within that library caused this error when I ran the parent project.
When I ran into this error, the solution for me was to prevent react and react-dom from being including in the some-library bundle output, by adding the following to the module.exports of its webpack.config.js:
externals: {
react: 'react',
'react-dom': 'react-dom'
}
If You use web pack then you can fix it by adding the following to Webpack config
for Don't bundle react or react-dom
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}

Categories

Resources