Using Webpack to include React from CDN, but not ReactDOM - javascript

In my webpack build, I would like to load React from a CDN, but not ReactDOM, as it requires an extra roundtrip for a very small file.
My webpack configuration has the following block of code declaring "externals" so that it will not build these files (I instead include CDNs).
webpack.config.js
...
externals: {
react: 'React'
},
...
The problem is that only including React in externals still builds React because ReactDOM depends on it.
node_modules/react-dom/index.js
module.exports = require('react/lib/ReactDOM');
Adding 'react-dom': 'ReactDOM' to externals effectively removes them both from the bundle, but I don't want to have to include the ReactDOM CDN...
How can I configure webpack to load React from a CDN, but include ReactDOM in my main bundle?
Note: I'm using webpack 2.1.0-beta17 and React 15.1.0.
Update
I tried adding react/lib/ReactDOM to externals.
...
externals: {
react: 'React',
'react/lib/ReactDOM': 'commonjs react-dom'
},
...
But I get the following error.
require is not defined

[Not an answer, too big for comment]
See issues https://github.com/facebook/react/issues/5413, https://github.com/facebook/react/issues/6128, which argue for having a CDN bundle of React+ReactDom, which would float your boat, if I understand correctly. Dan Abramov feels it, so I'm hopeful. Note, however, his comment that ReactDOM will get a lot bigger soon.

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

What's the recommended way to import libraries inside webpack.config.js?

I always used require call inside webpack.config.js to import libraries for my build workflow. However, recently I worked on a project where import is used.
Generally, it works fine but seems like some supporting tools that import webpack.config.js cannot import it when import is used inside of it with error:
Cannot use import statement outside a module.
As a tryout, I changed the config to run through babel and it worked, though for me looks like I am fine to use just good old require and official documentation still uses it.
Should I rollback import to avoid the caveats or I can use them without problem if I pass it through babel and renaming to webpack.config.babel.js?
After some research, I got to a solution - transpile webpack config before passing it to a library I used which failed to load it (eslint-import-resolver-webpack).
So, I don't load webpack.config.js directly, but via babel transpiler:
const webpackConfigTranspiled = babel.transformFileSync(
path.resolve(__dirname, './webpack.config.js'),
).code;
Then pass the transpiled content to the library/tool:
settings: {
'import/resolver': {
webpack: webpackConfigTranspiled,
},
'css-modules': {
basePath: 'src',
},
}

minify css using inline imports in webpack

We have lot of pages and lots of stylesheets. We wanted to inline some critical styles on some components to avoid network requests; I'd achieved the requirement by using style-loader with inline import in webpack 4 like this:
import React from "react";
import "./style.sass";
import FancyButton from "components/UIkit/FancyButton";
to: (!! at the beginning will disable all configured loaders)
import React from "react";
import "!!style-loader!css-loader!sass-loader!./style.sass";
import FancyButton from "components/UIkit/FancyButton";
this works fine and injects the scss into <style></style> tag; BUT in won't minfiy css.
I need a way to minify them too; preferably using inline import commands, magic comments or something like this:
!style-loader!require('mini-css-extract-plugin').loader!css-loader!less-loader!./style.less
our webpack config is ejected version create-react-app with slight modification to meet our needs. and for normal imports it works perfectly fine; and minifies all the csss using MiniCssExtractPlugin. but Inlining imports won't follow the default pipeline and hence no minification.
I also welcome other suggestions to inline critical styles; (consider they can be .scss, .less, .module.scss)
excluding this way:
Inline CSS using Webpack

React: Good Practices For CDNs, Node_Modules and Both

Been building a React application on top of create-react-app which uses Webpack through react-scripts and has React/Redux/React-dom, etc. as dependencies in our package.json.
To use those JS dependencies, we use import statements, however, for other JS dependencies (i.e. the JS used in Bootstrap and jQuery), we import those through CDN. (We have the Bootstrap CSS files in our CSS directory, and use import statements).
Is there a generally accepted practice to use CDNs vs node_modules or both on the front-end side?
My initial thought was to use CDNs to get static JS files if you're not going to use them as imports in your JS files, but wondered if that was the right way to think about it.
Obviously this isn't a React specific question, just using React to give more context.
Here's what I generally do:
(1) Setup webpack.config.js to assume global React and ReactDOM variables are available, to prevent them from being transpiled, minified, and bundled on every build. You can still use import statements in your files, as before, but the libraries will not be in your final bundle:
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
(2) Use a CDN like http://www.jsdelivr.com/, which allows you to get multiple scripts as a single concatenated string, in a single request. This will minimize loading time.
(3) (Optional) Install React and ReactDOM as deps, and have the build process copy the minified js files from node_modules/react(-dom)/dist to your project's public files directory, and then create simple checks for the variables at the foot of the page, to inject includes pointing to your server, if the CDN calls happen to fail. For example:
if (!window.React) {
$('head').append('<script src="/js/react.min.js">')
}

Import from subfolder of npm package

I've been working on creating a small library of React components for use in several other projects. I am publishing the package internally (using a private GitHub repository) and then including in another project. However, when I go to import from a subdirectory of the package I am not able to do so as the paths don't match.
The projects using the package all utilize webpack to bundle/transpile code as I am trying to avoid doing any building in the component library if possible.
Directory Structure
- package.json
- src/
- index.js
- Button/
- index.js
- Button.jsx
- ButtonGroup.jsx
- Header/
- index.js
- Header.jsx (default export)
package.json
...
"main": "./src/index.js",
"scripts": "",
...
src/Button/index.js
import Button from './Button';
import ButtonGroup from './ButtonGroup';
export default Button;
export { Button, ButtonGroup};
src/index.js
Is this file actually necessary if only importing from subdirectories?
import Button from './Button';
import ButtonGroup from './Button/ButtonGroup';
import Header from './Header';
export { Button, ButtonGroup, Header };
Other Project
// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';
Example
Material-UI is a library of React components that is used by requiring in the following fashion: import { RadioButtonGroup } from 'material-ui/RadioButton. I've tried to figure out how this works for them but to no avail yet.
Similar Questions
How would I import a module within an npm package subfolder with webpack?
This is very nearly the correct approach I require, except that the import path used there involved the src/ directory, which I am trying to avoid (should be component-library/item, not component-library/src/item (which does work currently though))
Publishing Flat NPM Packages
This is exactly what I want except that I was hoping to not have a "build" phase in the package (rely on importing locations to build/transpile)
Questions
Can I skip the src/ directory somehow in the import path?
Can I skip any type of build phase in the package (so developers don't have to build before committing)?
How does a package similar to material-ui handle this?
Can I skip the src/ directory somehow in the import path?
Yes. Using the package.json "exports" field, which should be supported by Webpack in a near future (see this issue), but has already been supported by Node since Node 12 LTS following the Bare Module Specifier Resolution proposal:
package.json
...
"main": "./src/index.js",
"type": "module",
...
"exports": {
"./Button": "./src/Button/index.js",
"./Header": "./src/Header/index.js"
},
...
Now, the following code:
// This project is responsible for building/transpiling after importing
import { Button, ButtonGroup } from 'components-library/Button';
should be translated to:
import { Button, ButtonGroup } from 'components-library/src/Button/index.js';
which should correctly import the requested modules.
Caveat
Now, it would certainly be tempting to try a simpler version like:
...
"exports": {
"./Button": "./src/Button/",
"./Header": "./src/Header/"
},
...
so as the usual import statement
import { ... } from 'components-library/Button';
gets translated to
import { ... } from 'components-library/src/Button';
This looks nice, but it will not work in this case, because your submodules don't have each their own package.json file but rely on their index.js file to be found.
/!\ Unlike in CommonJS, there is no automatic searching for index.js or index.mjs or for file extensions.
src/index.js - Is this file actually necessary if only importing from subdirectories?
I don't think so, but you can keep it if you want.
Can I skip any type of build phase in the package?
Using the "exports" field does not require you to transpile your code.
The answer may depend on how you installed your components library. If you did it via either npm install <git-host>:<git-user>/<repo-name> or npm install <git repo url>,
You should be able to import {Button} from 'component-library/Button' as is, according to your first linked question. Similar to Node's require() resolution, Webpack should resolve subdirectories within component-library relative to component-library's entry point. You can find the docs on customizing the resolution behavior via the webpack.config.resolve property. material-ui seems to rely on resolving subdirectory imports from the module entry directory.
To distribute an ES module library, there's no need for building before distribution. However, projects such as create-react-app may need a pre-transpiled version.
Alternately, you can write import {Button} from 'components-library'.
Webpack will trace the dependencies back through each index without a fuss.
you have to install babel-plugin-module-resolver package
Specify the package relative path in your .babelrc file alias like this
{
"plugins": [
["module-resolver", {
"alias": {
"components-library": "./node_module/components-library"
}
}]
]
}
then you can import subdir of npm package like this
import { Button, ButtonGroup } from 'components-library/Button';
One of the possible solutions there is webpack aliasing system.
You can create another project, call it for example 'app-aliases', so your aliases will be reusable.
This project will has one js file with all of your packages paths:
const path = require('path');
module.exports = {
'#components': path.resolve(__dirname, 'node_modules/components-library/src'),
'#another': path.resolve(__dirname, 'node_modules/any/path/you/want'),
}
And then add it to the webpack configuration in any project which will be responsible for building/transpiling:
webpack.config.js
const appAliases = require('app-aliases');
const config = {
...
resolve: {
alias: {
...appAlises
}
}
}
In the runtime code you will be able to use it like this:
import {Button} from '#components/Button';
import {Something} from '#another'
If you are using typescript you will need to add the same aliases to the paths tsconfig property.
So answers to your questions are:
Yes, you can use any path in aliases
Yes, it is not necessary to build all of your projects
I see that now mui uses imports from directi packages (core for example), see https://material-ui.com/components/radio-buttons/ there is import Radio from '#material-ui/core/Radio';. But I hope they using re-export that I described below.
Also about node.js resolution mechanism.
When you import some library it tries to find node_modules/some-library/package.json and then main property inside it. This property should lead to your main entry point. Usually it is src/index.js (you should set it in package.json if it is no there yet). In this file you can re-export anything you want from internals file structure and will be able to use it without the full path.
Please see this repo for some examples.
I'am an angular developer never used react but what I could tell that material-ui are using monorepo where same concept exists in angular where we create one workspace and this workspace hold multiple project/packages as named in react. for more info Workspaces with Yarn
Material-ui using fake paths in tsconfig to make it appears like src folder doesn't exists this from the git you provided: tsconfig.json
This is possible but requires publishing a curated dist folder rather then the root of your project.
The whole thing is rather simple if you understand how module resolution works, and you just need a small script to prepare your distribution.
Lest I repeat all the details here, please see my answer for Importing from subfolders for a javascript package.

Categories

Resources