How to make cherry picking react component library like lodash & date/fns - reactjs

I have made one small library which includes 20-25 different components & made an npm package of it.
my react project, where I want to use these components, has many pages[routes] used lazy-loading so each page has its own bundle.
but when I write the statement on my home page[App.js].
import { MyModal } from 'my-react-lib';
each and every component is loaded into home page bundle. so my initial loading performance is worst. [2Mb initial bundle size]
I have read the concept of tree shaking and event tried to implement in webpack & even with rollup but they only make bundle.js but not as per mine requirement.
I am willing to achieve cherry-picking like import-export. same as date-fns & lodash does.
import format from 'date-fns/format';
import debounce from 'lodash/debounce';
how to achieve this?
import MyModal from 'my-react-lib/MyModal';
import OtherComponent from 'my-react-lib/OtherComponent';

Rollup allows you to split your library into several independent chunks. You have to provide an object, mapping names to entry points, to the input property of the rollup configuration. It looks like this:
input: {
index: 'src/index.js',
theme: 'src/Theme',
badge: 'src/components/Badge',
contentCard: 'src/components/ContentCard',
card: 'src/elements/Card',
icon: 'src/elements/Icon',
...
src/index.js looks like this:
export { default as Theme } from './Theme'
export { default as Badge } from './components/Badge'
...
Have a look at rollup’s documentation: https://rollupjs.org/guide/en/#input
The output is set to a directory:
output: [
{
dir: 'dist/es',
format: 'es',
},
],
Then you declare the entry point in your package.json as follows:
"module": "dist/es/index.js",
The modules of your library can then be imported:
import { Theme, Badge } from 'your-component-library'

You may need to package them separately.
For example the Material-UI, there are many parts of it. When we use it in the normal way
npm install #material-ui/core
If you look at their source, you would find out that there are multiple packages, each with it's own package.json file
For example the #material-ui/core pacakge.json
{
"name": "#material-ui/core",
"version": "4.9.7",
"private": false,
"author": "Material-UI Team",
...
"dependencies": {
"#babel/runtime": "^7.4.4",
"#material-ui/styles": "^4.9.6",
"#material-ui/system": "^4.9.6",
"#material-ui/types": "^5.0.0",
"#material-ui/utils": "^4.9.6",
Which means that they are actually separate and have dependencies with each other.
Well, that's the scope.

Related

Tree shaking not working for the react component library using named imports

Our company projects count is growing, so we decided to move our ui kit to a separate private npm package. For building kit before publishing we decided to use rollup.
The file structure for ui kit is quite standard we've:
src
- Components
- Alert
- Button
- Heading
- ...
HOC, static...
- index.js
Root index.js file is used only for reexporting all components.
export {default as Alert} from './Components/Alert';
export {default as Button} from './Components/Button';
...
Our main goal is to be able to get components from our kit using named imports.
import {Alert, Button} from '#company/ui-kit';
Properly working tree shaking is also important, because some of our projects are using only 2/31 components from the kit, so we want to see in final build only components what has been used and this is the main challenge. We tried many options of rollup config with single output file or chunks and realized, that tree shaking only works with chunks and what's more interesting it works only with importing chunk directly, not named import from the index.js.
import Alert from '#company/ui-kit/Alert'; // tree shaking works, only Alert get into build
import {Alert} from '#company/ui-kit'; // tree shaking not working, all kit components are got into the build
Could somebody explain me why this is happening? And is there any way to use named imports with properly working tree shaking. What is also very curious, that we tried to test tree shaking with named imports in some popular libraries like material ui and there it's not working too. Tree shaking works only with direct import from chunk.
Also need to mention that we tested tree shaking only in projects created with create-react-app. I don't really think the problem could be inside create-react-app bundler config, but maybe it will help to figure out this.
For the full picture also attaching our rollup config:
import multiInput from 'rollup-plugin-multi-input';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import external from 'rollup-plugin-peer-deps-external';
import resolve from 'rollup-plugin-node-resolve';
import copy from 'rollup-plugin-copy'
import postcss from 'rollup-plugin-postcss';
import postcssUrl from 'postcss-url';
import asset from "rollup-plugin-smart-asset";
export default {
input: ['src/index.js', 'src/Components/**/*.js'],
output: [
{
dir: './',
format: 'cjs',
sourcemap: true,
exports: 'auto',
chunkFileNames: '__chunks/[name]_[hash].js'
}
],
plugins: [
multiInput({relative: 'src/'}),
external(),
postcss({
modules: false,
extract: true,
minimize: true,
sourceMap: true,
plugins: [postcssUrl({url: 'inline', maxSize: 1000})],
}),
copy({targets: [{src: 'src/static/icons/fonts/*', dest: 'fonts'}]}),
babel({exclude: 'node_modules/**'}),
asset({url: 'copy', keepImport: true, useHash: false, keepName: true, assetsPath: 'assets/'}),
resolve(),
commonjs({include: 'node_modules/**'})
]
};
Thanks in advance for your replies!

Move fontawesome to own bundle using dynamic import

I am trying to reduce my bundle size by splitting it into several chunks.
BundleAnalyzerPlugin tells me that Fontawesome is imported in full even though I have tried to only import the icons I need which seems odd.
My fontawesome imports
import { library } from '#fortawesome/fontawesome-svg-core';
import { faBell, faEyeSlash, faEye} from '#fortawesome/free-solid-svg-icons';
import { faBell as regularBell} from '#fortawesome/free-regular-svg-icons';
...
library.add( faBell, faEye, faEyeSlash, regularBell, regularCalendarAlt)
My fontawesome version
"#fortawesome/fontawesome": "^1.1.5",
"#fortawesome/fontawesome-free-regular": "^5.0.10",
"#fortawesome/fontawesome-free-solid": "^5.0.10",
"#fortawesome/fontawesome-svg-core": "^1.2.8",
"#fortawesome/react-fontawesome": "^0.1.3",
I am trying to use the import() splitting technique described here but can't figure out how to make it work for Fontawesome.
return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
...
}
How can I put split fontawesome into it's own bundle and load it synchronously?
Something along the lines of Prefetching/Preloading would work too, but those techniques seems to have very poor browser support? I sadly need support for Safari.
My webpack config
// Set up webpack plugins
config.plugins = [
nodeEnvPlugin,
firebasePlugin,
cssExtractPlugin,
new BundleAnalyzerPlugin(
{
excludeAssets: /node_modules/,
statsOptions: {
exclude: /node_modules/
}
}
)
]
Kind regards /K
You're trying to view node_modules which shouldn't be part of your bundle upon deployment. You should be configuring your BundleAnalyzerPlugin to view the destination of webpack output instead. That way you could fully identify which are consuming big in terms of size.
Here's the option about it from your library of choice:
https://github.com/webpack-contrib/webpack-bundle-analyzer#bundledir
Here is mine implementing the same technique you have above on fontawesome optimization
Here's my repo for your reference: https://github.com/crrmacarse/crrmacarse.github.io

storybook #storybook/addon-options dosent work

I am trying to set some option but it doesn't work.
package.json
"devDependencies": {
"#storybook/addon-actions": "^3.4.10",
"#storybook/addon-links": "^3.4.10",
"#storybook/addon-options": "^3.4.11",
"#storybook/addon-storyshots": "^3.4.10",
"#storybook/addons": "^3.4.10",
"#storybook/react": "^3.4.11"
}
addons.js
import '#storybook/addon-options/register';
config.js
import { configure } from '#storybook/react';
import { setOptions } from '#storybook/addon-options';
setOptions({ name: 'my name' });
// automatically import all files ending in *.stories.js
const req = require.context('../stories', true, /.stories.js$/);
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
configure(loadStories, module);
I am trying to understand where i s, going wrong for a few hours.
Edit after testing with provided versions:
I tested this locally with the above versions, and what it looks like is that because #storybook/react and #storybook/addons are on different versions, even by a patch, #storybook/react ends up installing it's own addons dependency and the 2 become out of sync.
If this is the case, you will likely see an error like "accessing nonexistent addons channel" in the console.
To fix this you will need to increase the version of the addons dependency to v3.4.11 AND reinstall the react dependency.
npm install --save-dev #storybook/addons#3.4.11 #storybook/react#3.4.11
Note: If you only update addons to v3.4.11 without reinstalling the react dependency, it won't fully sync up, because the react would already have installed it's own addon subdependency.
An image of the filesystem within node_modules directoy:
Previously Answered for Storybook v4:
According to the documentation, you have to apply the settings as a decorator (and there is no setOptions function, but there is a withOptions function).
So try this:
import { addDecorator, configure } from '#storybook/react';
import { withOptions } from '#storybook/addon-options';
addDecorator(withOptions({ name: 'my name' }));
// rest of the config goes here
Also, make sure to register the addon by adding the following line in your addons.js file:
import '#storybook/addon-options/register';
Reference: https://www.npmjs.com/package/#storybook/addon-options
Aside: The reason it has to be within a decorator is because the UI of storybook is rendered as part of each story, so you can't just set options globally without having each story apply those settings.

ant design - huge imports

I'm using ant design library for my react application.
And I've faced with huge imports, that hurts my bundle (currently 1.1 mb in minified version because of ant-design lib).
How can I differently import antd components through all my app?
UPDATE:
Seems antd has some huge or non optimized modules.
Here the thing - only difference is import Datepicker module, and.. boom! + almost 2MB (in dev bundle ofc.)
UPD: the underlying issue seems to be resolved for the new (4.0) version of antd.
Therefore, if you try to resolve this issue for the earlier versions, the recommended way is to migrate onto antd 4
Previous answer:
At the moment, a huge part of antd dist is SVG icons.
There is no official way to deal with it yet (check the issue on github).
But a workaround exists.
Adapt webpack to resolve icons differently. In your webpack config:
module.exports = {
//...
resolve: {
alias: {
"#ant-design/icons/lib/dist$": path.resolve(__dirname, "./src/icons.js")
}
}
};
Create icons.js in the folder src/ or wherever you want it. Be sure it matches the alias path!
In this file, you define which icons antd should include.
export {
default as DownOutline
} from "#ant-design/icons/lib/outline/DownOutline";
It's also possible to do this with react-app-rewired (create-react-app modifications) within config-overrides.js
1) Prevent antd to load the all moment localization.
Add webpack plugin and configure it in webpack.config.js like the follow:
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /ru/),
],
resolve: {
alias: {moment: `moment/moment.js`}
},
target: `web`
}
2) Use the same moment version as in antd library.
3) Use modularized antd
Use babel-plugin-import
// .babelrc or babel-loader option
{
"plugins": [
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
// `style: true` for less
]
}
I use BundleAnalyzerPlugin to analyze the bundle.
plugins: [new BundleAnalyzerPlugin()]
Looking at the docs
https://ant.design/docs/react/getting-started#Import-on-Demand
there is a recommedation to import individual components on demand.
So, you can try and replace
import { Button} from 'antd'
with
import Button from 'antd/lib/button'
I reduced my bundle size by 500KB by editing config-override.js like so:
config-override.js
const { override, fixBabelImports } = require('customize-cra');
const path = require('path');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css'
}),
// used to minimise bundle size by 500KB
function(config, env) {
const alias = config.resolve.alias || {};
alias['#ant-design/icons/lib/dist$'] = path.resolve(__dirname, './src/icons.js');
config.resolve.alias = alias;
return config;
}
);
./src/icons.js
/**
* List all antd icons you want to use in your source code
*/
export {
default as SearchOutline
} from '#ant-design/icons/lib/outline/SearchOutline';
export {
default as CloseOutline
} from '#ant-design/icons/lib/outline/CloseOutline';
export {
default as QuestionCircleOutline
} from '#ant-design/icons/lib/outline/QuestionCircleOutline';
export {
default as PlayCircleOutline
} from '#ant-design/icons/lib/outline/PlayCircleOutline';
export {
default as PauseCircleOutline
} from '#ant-design/icons/lib/outline/PauseCircleOutline';
export {
default as LoadingOutline
} from '#ant-design/icons/lib/outline/LoadingOutline';
Before
After
Those few components are certainly not 1.2M together. Looks like you are importing the whole library when you only need a few components.
To get antd to load only the needed modules you should use babel-plugin-import. Check your console log for the "You are using a whole package of antd" warning described at that link.
Check out the docs for Create-React-App for how to implement it if you're using CRA.
Try using code splitting using webpack and react router. It will help you to load the modules asynchronously. This is the only solution helped me to improve the page load time when using ant framework.
Issue which caused large bundle size has been fixed in Ant Design 4.0.
Quoting from the release announcement.
Smaller size
In antd # 3.9.0, we introduced the svg icon ([Why use the svg icon?]
()). The icon API
using the string name cannot be loaded on demand, so the svg icon file
is fully introduced, which greatly increases the size of the packaged
product. In 4.0, we adjusted the icon usage API to support tree
shaking, reducing the default package size of Antant by about 150 KB
(Gzipped).
In order to install Ant Design 4 you have to do following
npm install antd#4.0.0-rc.1
// or in yarn
yarn add antd#4.0.0-rc.1

TypeError: __webpack_require__.i(...) is not a function

I am getting a webpack TypeError when I am trying to simplify an import. The following code works without any issues. Here I am importing a React Higher-Order-Component (HOC) called smartForm from core/components/form/index.js.
core/components/form/index.js (does a named export of smartForm)
export smartForm from './smart-form';
login-form.jsx (imports and uses smartForm)
import { smartForm } from 'core/components/form';
class LoginForm extends React.Component {
...
}
export default smartForm(LoginForm);
However, I want to simplify the import to just import { smartForm } from 'core'. So I re-exported smart-form in core/index.js and imported it from core. See the code below:
core/index.js (does a named export of smartForm)
export { smartForm } from './components/form';
// export smartForm from './components/form'; <--- Also tried this
login-form.jsx (imports and uses smartForm)
import { smartForm } from 'core';
class LoginForm extends React.Component {
...
}
export default smartForm(LoginForm); // <--- Runtime exception here
This code compiles without any issues, but I am getting the following runtime exception at the line export default smartForm(LoginForm);:
login-form.jsx:83 Uncaught TypeError: webpack_require.i(...) is not a function(…)
What am I missing?
P.S. Here are the Bable and plugin versions that I am using:
"babel-core": "^6.18.2",
"babel-preset-es2015-webpack": "^6.4.3",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-1": "^6.16.0",
tl;dr
For the questioner: Add this to your webpack.config.js:
resolve: {
alias: {
core: path.join(__dirname, 'core'),
},
},
For the general audience: Make sure the thing you try to import is indeed exists in that package.
Explanation
Questioner's problem: importing own code like npm modules
You try to import your module's exports in the same fashion how you import something from an npm package from the node_modules folder: import { something } from 'packagename';. The problem with this is that doesn't work out of the box. The Node.js docs give the answer on why:
Without a leading '/', './', or '../' to indicate a file, the module must either be a core module or is loaded from a node_modules folder.
So you either has to do what Waldo Jeffers and Spain Train suggested and write import { smartForm } from './core', or you can configure webpack so it can resolve your import path via its aliases which are created to solve this exact problem.
Debugging the error message in general
This error can arise if you try to import something from an existing npm package (in node_modules) but the imported thing doesn't exist in the exports. In this case, make sure there were no typos and that the given thing you try to import is indeed in that package. Nowadays it is trendy to break libraries into multiple npm packages, you might be trying to import from a wrong package.
So if you get something like this:
TypeError: __webpack_require__.i(...) is not a function
at /home/user/code/test/index.js:165080:81
at Layer.handle [as handle_request] (/home/user/code/test/index.js:49645:5)
To get more information on what import you should check, just open the output file generated by webpack and go to the line marked by the topmost line in the error stack (165080 in this case). You should see something like: __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_9_react_router_dom__["match"]). This tells us that there is no match export (or there is but it isn't a function) in react-router-dom, so we need to check our stuff around that import.
Since core is a folder of your app and not an npm module, Webpack can not understand which file you want to load. You must use a correct path pointing to a file. You have to replace import { smartForm } from 'core'; by import { smartForm } from './core/index.js
This error will occur by many reasons. Once I encountered this error when I add js in file-loader then when I removed it start to work correctly.
{
test: /\.(ttf|eot|svg|gif|jpg|png|js)(\?[\s\S]+)?$/,
use: 'file-loader'
},
When I remove |js it works
{
test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/,
use: 'file-loader'
},
Just remove these 2 line from config
const context = resolve.alias.context('./', true, /\.spec\.ts$/); context.keys().map(context);

Resources