Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shared component library best practices

I am creating a shareable React component library.

The library contains many components but the end user may only need to use a few of them.

When you bundle code with Webpack (or Parcel or Rollup) it creates one single file with all the code.

For performance reasons I do not want to all that code to be downloaded by the browser unless it is actually used. Am I right in thinking that I should not bundle the components? Should the bundling be left to the consumer of the components? Do I leave anything else to the consumer of the components? Do I just transpile the JSX and that's it?

If the same repo contains lots of different components, what should be in main.js?

like image 430
Ollie Williams Avatar asked Jan 24 '20 16:01

Ollie Williams


Video Answer


1 Answers

This is an extremely long answer because this question deserves an extremely long and detailed answer as the "best practice" way is more complicated than just a few line response.

Iv'e maintained our in house libraries for 3.5+ years in that time iv'e settled on a two ways i think libraries should be bundled the trade offs depend on how big your library is and personally we compile both ways to please both subsets of consumers.

Method 1: Create a index.ts file with everything you want exposed exported and target rollup at this file as its input. Bundle your entire library into a single index.js file and index.css file; With external dependency's inherited from the consumer project to avoid duplication of library code. (gist included at bottom of example config)

  • Pros: Easy to consume as project consumers can import everything from the root relative library path import { Foo, Bar } from "library"
  • Cons: This will never be tree shakable; and before people say do this with ESM and it will be treeshakeable. NextJS doesn't support ESM at this current stage and neither do alot of project setups that's why its still a good idea to compile this build to just CJS. If someone imports 1 of your components they will get all the css and all the javascript for all your components.

Method 2: This is for advanced users: Create a new file for every export and use rollup-plugin-multi-input with the option "preserveModules: true" depending on how what css system you're using your also need to make sure that your css is NOT merged into a single file but that each css file requires(".css") statement is left inside the output file after rollup and that css file exists.

  • Pros: When users import { Foo } from "library/dist/foo" they will only get the code for Foo, and the css for Foo and nothing more.
  • Cons: This setup involves the consumer having to handle node_modules require(".css") statements in their build configuration with NextJS this is done with next-transpile-modules npm package.
  • Caveat: We use our own babel plugin you can find here: https://www.npmjs.com/package/babel-plugin-qubic to allow people to import { Foo,Bar } from "library" and then with babel transform it to...
import { Foo } from "library/dist/export/foo"
import { Bar } from "library/dist/export/bar"

We have multiple rollup configurations where we actually use both methods; so for library consumers who don't care for tree shaking can just do "Foo from "library" and import the single css file; and for library consumers who do care for tree shaking and only using critical css they can just turn on our babel plugin.

Rollup guide for best practice:

whether you are using typescript or not ALWAYS build with "rollup-plugin-babel": "5.0.0-alpha.1" Make sure your .babelrc looks like this.

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {"chrome": "58", "ie": "11"},
      "useBuiltIns": false
    }],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "absoluteRuntime": false,
      "corejs": false,
      "helpers": true,
      "regenerator": true,
      "useESModules": false,
      "version": "^7.8.3"
    }],
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-classes",
    ["@babel/plugin-proposal-optional-chaining", {
      "loose": true
    }]
  ]
}

And with the babel plugin in rollup looking like this...

        babel({
            babelHelpers: "runtime",
            extensions,
            include: ["src/**/*"],
            exclude: "node_modules/**",
            babelrc: true
        }),

And your package.json looking ATLEAST like this:

    "dependencies": {
        "@babel/runtime": "^7.8.3",
        "react": "^16.10.2",
        "react-dom": "^16.10.2",
        "regenerator-runtime": "^0.13.3"
    },
    "peerDependencies": {
        "react": "^16.12.0",
        "react-dom": "^16.12.0",
    }

And finally your externals in rollup looking ATLEAST like this.

const makeExternalPredicate = externalArr => {
    if (externalArr.length === 0) return () => false;
    return id => new RegExp(`^(${externalArr.join('|')})($|/)`).test(id);
};

//... rest of rollup config above external.
    external: makeExternalPredicate(Object.keys(pkg.peerDependencies || {}).concat(Object.keys(pkg.dependencies || {}))),
// rest of rollup config below external.

Why?

  • This will bundle your shit to automatically to inherit react/react-dom and your other peer/external dependencies from the consumer project meaning they wont be duplicated in your bundle.
  • This will bundle to ES5
  • This will automatically require("..") in all the babel helper functions for objectSpread, classes etc FROM the consumer project which will wipe another 15-25KB from your bundle size and mean that the helper functions for objectSpread wont be duplicated in your library output + the consuming projects bundled output.
  • Async functions will still work
  • externals will match anything that starts with that peer-dependency suffix i.e babel-helpers will match external for babel-helpers/helpers/object-spread

Finally here is a gist for an example single index.js file output rollup config file. https://gist.github.com/ShanonJackson/deb65ebf5b2094b3eac6141b9c25a0e3 Where the target src/export/index.ts looks like this...

export { Button } from "../components/Button/Button";
export * from "../components/Button/Button.styles";

export { Checkbox } from "../components/Checkbox/Checkbox";
export * from "../components/Checkbox/Checkbox.styles";

export { DatePicker } from "../components/DateTimePicker/DatePicker/DatePicker";
export { TimePicker } from "../components/DateTimePicker/TimePicker/TimePicker";
export { DayPicker } from "../components/DayPicker/DayPicker";
// etc etc etc

Let me know if you experience any problems with babel, rollup, or have any questions about bundling/libraries.

like image 122
Shanon Jackson Avatar answered Oct 11 '22 10:10

Shanon Jackson