My Experience Developing a React Component Library with Rollup.js
My Experience Developing a React Component Library with Rollup.js
As one of the tasks assigned to me at work, I was given the job of turning components into a library and publishing them to npm. In other words, I needed to develop a component library like react-bootstrap.
My only prior experience with code publishing was deploying a JAVA open-source library to Maven once, so publishing in this new development environment inevitably led to trial and error.
What I felt while developing was that there just weren't many deep references to look at, and I couldn't find suitable code I could just reuse. Fortunately, by grinding through it over time, I was able to establish some kind of foundation.
It was fairly fun, seemed worth digging into, and since there didn't seem to be a widely-known reference for it either, I decided to try making one myself.
- Not using Create React App, for minimal bundling
- Build a TypeScript-based React library development environment
- Use SCSS for style code (CSS-in-CSS)
- Test components with Storybook
- Publish to npm and verify usability in other projects
These are the goals. The ultimate goal is to provide a development environment that's usable to a reasonable degree, even if it doesn't reach a professional-grade level.
I named this project React Components Library Starter. I wanted the fact that it provides a React library development environment to come through intuitively.
Unless it's productized software or a solution, I think it's best for this kind of library that, just by looking at the name, you can roughly guess "oh, this is a library that does X."
Since I'm not using CRA, I build it entirely from scratch.
This is written based on yarn.
BASH
mkdir react-components-library-starter cd react-components-library-starter yarn init # question name (react-components-library-starter): # question version (1.0.0): # question description: # question entry point (index.js): # question repository url (https://github.com/itcode-dev/react-components-library-starter): # question author (RWB0104 <psj2716@gmail.com>): # question license (MIT): # question private: false mkdir src
- Create the folder.
- Initialize the project.
- Create the src folder. This will be the top-level folder for the source code.
BASH
yarn add -D react @types/react
| Name | Purpose |
|---|---|
| react | React library |
| @types/react | React library types |
- Install the React-related libraries.
- Most libraries not directly related to running this library are installed as devDependencies by specifying the -D option.
BASH
yarn add -D typescript
| Name | Purpose |
|---|---|
| typescript | TypeScript library |
- Install the TypeScript-related library.
BASH
vim tsconfig.json
- To configure the TypeScript build settings, create a config file tsconfig.json.
JSON
{ "compilerOptions": { "target": "es5", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "jsx": "react", "module": "ESNext", "declaration": true, "declarationDir": "./dist", "sourceMap": false, "outDir": "./dist", "moduleResolution": "node", "allowSyntheticDefaultImports": true, "emitDeclarationOnly": true, "removeComments": true }, "include": [ "./src" ], "exclude": [ "./dist", "./node_modules", "./src/**/*.test.tsx", "./src/**/*.stories.tsx", ] }
- An example of tsconfig.json is shown above.
- declaration - whether to generate *.d.ts type files
- declarationDir - the output path for *.d.ts. Must be the same as, or a subpath of, output.
- sourceMap - whether to generate source map code for bundle analysis
- outDir - the output path. The library's build artifacts are generated in dist/.
- Unless you need special settings, it's fine to just use it as-is.
BASH
yarn add classnames style-inject yarn add -D postcss sass
| Name | Purpose |
|---|---|
| classnames | Library for joining the className class attribute |
| style-inject | Style tag header injector |
| postcss | CSS post-processor |
| sass | SASS/SCSS library |
BASH
yarn add -D rollup @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript rollup-plugin-peer-deps-external rollup-plugin-postcss
| Name | Purpose |
|---|---|
| rollup | Rollup.js core |
| @rollup/plugin-babel | Plugin for integrating Rollup.js with Babel |
| @rollup/plugin-commonjs | Plugin that converts CommonJS -> ES6 code |
| @rollup/plugin-node-resolve | A plugin that, when using an external library, converts references to resolve against the node_modules of the project the library is installed in |
| @rollup/plugin-typescript | Plugin for integrating Rollup.js with TypeScript |
| rollup-plugin-peer-deps-external | A plugin that avoids bundling peerDependencies modules and instead converts them to resolve against the node_modules of the project the library is installed in |
| rollup-plugin-postcss | Plugin for integrating Rollup.js with PostCSS |
- Install the Rollup.js-related libraries.
- You can add various plugins depending on the developer's needs.
BASH
vim rollup.config.js
- To configure Rollup.js, create rollup.config.js.
JS
/** * Rollup configuration module * * @author RWB * @since 2022.06.06 Mon 17:44:31 */ import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import postcss from 'rollup-plugin-postcss'; const extensions = [ 'js', 'jsx', 'ts', 'tsx', 'mjs' ]; const pkg = require('./package.json') const config = [ { external: [ /node_modules/ ], input: './src/index.ts', output: [ { dir: './dist', format: 'cjs', preserveModules: true, preserveModulesRoot: 'src' }, { file: pkg.module, format: 'es' } , { name: pkg.name, file: pkg.browser, format: 'umd' } ], plugins: [ nodeResolve({ extensions }), babel({ exclude: 'node_modules/**', extensions, include: [ 'src/**/*' ] }), commonjs({ include: 'node_modules/**' }), peerDepsExternal(), typescript({ tsconfig: './tsconfig.json' }), postcss({ extract: false, inject: (cssVariableName) => `import styleInject from 'style-inject';\nstyleInject(${cssVariableName});`, modules: true, sourceMap: false, use: [ 'sass' ] }) ] } ]; export default config;
- The config example is shown above.
- Output the result based on the ./src/index.ts file
- CJS (default)
- ESM
- UMD
- The CJS module supports Tree Shaking via preserveModules
- @rollup/plugin-babel must run before @rollup/plugin-commonjs
- Due to the nature of plugins, ordering can matter significantly
BASH
npx storybook init --builder webpack5 yarn add -D @storybook/preset-scss css-loader sass-loader style-loader react-dom
| Name | Purpose |
|---|---|
| storybook | Storybook CLI |
| @storybook/preset-scss | Storybook's webpack SCSS configuration addon |
| css-loader | CSS resolver |
| sass-loader | Library that builds SASS/SCSS into CSS |
| style-loader | Library that injects CSS code into the DOM |
| react-dom | React DOM handler |
- Install the libraries needed to run Storybook.
- .storybook/ - Storybook config folder
- src/stories/ - Storybook demo folder
- Since the latest versions of most style-related loaders are compatible with webpack5, you must set Storybook's builder to webpack5.
- The default builder is webpack4, in which case the loader versions need to be downgraded to match the builder.
Installation alone isn't enough; some simple configuration is also required.
Running npx storybook init --builder webpack5 automatically installs Storybook into the project. During this process, it creates a .storybook folder at the top-level path.
Add the following code to .storybook/main.js.
JS
module.exports = { "stories": [ "../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)" ], "addons": [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", // Added "@storybook/preset-scss" ], "framework": "@storybook/react", "core": { "builder": "@storybook/builder-webpack5" } }
- Apply @storybook/preset-scss by adding it to the addon list.
Consistent code rules improve code readability. Of course, a developer could manually follow code conventions entirely by hand, but since it's still humans doing the work, mistakes happen, and manually hunting down code that doesn't match conventions requires a lot of effort with almost no correlation to code performance. On top of that, when there are multiple developers, code conventions can easily break down due to each person's subjective judgment.
Using ESLint lets you follow code conventions without developers needing to think about it.
However, this is purely for maintaining code quality, has little to no connection to code performance, and having or not having ESLint is completely unrelated to development. If you'd rather not bother with this at all, feel free to skip this section — it won't affect the library's development going forward in any way.
BASH
yarn add -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-sort-keys-fix eslint-plugin-storybook
| Name | Purpose |
|---|---|
| eslint | ESLint core |
| @typescript-eslint/eslint-plugin | Plugin for applying ESLint to a TypeScript environment |
| @typescript-eslint/parser | ESLint TypeScript parser plugin |
| eslint-config-airbnb | Airbnb rule configuration |
| eslint-plugin-import | import/export rules plugin |
| eslint-plugin-jsx-a11y | JSX element rules plugin |
| eslint-plugin-react | React rules plugin |
| eslint-plugin-react-hooks | React Hook rules plugin |
| eslint-plugin-sort-keys-fix | Object key sorting rules plugin |
| eslint-plugin-storybook | Storybook rules plugin |
- Install ESLint along with its related configuration and plugins.
JS
module.exports = { env: { browser: true, node: true }, extends: [ 'airbnb', 'airbnb/hooks', 'eslint:recommended', 'plugin:react/recommended', 'plugin:import/recommended', 'plugin:storybook/recommended' ], ignorePatterns: [ '.storybook', '*.d.ts', 'node_modules', 'build', 'dist', '**/env/*.js' ], overrides: [ { files: [ '*.ts', '*.tsx' ], rules: { 'no-undef': 'off' } } ], parser: '@typescript-eslint/parser', parserOptions: { warnOnUnsupportedTypeScriptVersion: false }, plugins: [ '@typescript-eslint', 'sort-keys-fix', 'prettier' ], rules: { '@typescript-eslint/ban-ts-comment': [ 'error', { 'ts-ignore': 'allow-with-description' } ], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-unused-vars': 'error', 'array-bracket-spacing': [ 'error', 'always', { arraysInArrays: false, objectsInArrays: false } ], 'brace-style': [ 'error', 'allman' ], 'comma-dangle': [ 'error', 'never' ], 'eol-last': [ 'error', 'never' ], 'import/extensions': 'off', 'import/named': 'off', 'import/no-anonymous-default-export': 'off', 'import/no-cycle': 'off', 'import/no-extraneous-dependencies': 'off', 'import/no-named-as-default': 'off', 'import/no-unresolved': 'off', 'import/order': [ 'error', { alphabetize: { caseInsensitive: true, order: 'asc' }, groups: [ 'external', 'builtin', 'internal', 'sibling', 'parent', 'index' ], 'newlines-between': 'always' } ], indent: [ 'error', 'tab' ], 'jsx-a11y/control-has-associated-label': 'off', 'jsx-quotes': [ 'error', 'prefer-single' ], 'linebreak-style': 'off', 'max-len': 'off', 'no-restricted-exports': 'off', 'no-tabs': [ 'error', { allowIndentationTabs: true }], 'no-unused-vars': 'off', 'object-curly-newline': [ 'error', { ExportDeclaration: 'never', ImportDeclaration: 'never', ObjectExpression: { minProperties: 3, multiline: true }, ObjectPattern: 'never' }], 'react-hooks/exhaustive-deps': 'warn', 'react/button-has-type': 'off', 'react/destructuring-assignment': 'off', 'react/function-component-definition': 'off', 'react/jsx-curly-brace-presence': [ 'error', { children: 'never', props: 'never' } ], 'react/jsx-filename-extension': 'off', 'react/jsx-indent': [ 'error', 'tab' ], 'react/jsx-props-no-spreading': 'off', 'react/jsx-sort-props': [ 'error', { callbacksLast: true, ignoreCase: true, multiline: 'last', noSortAlphabetically: false, reservedFirst: false, shorthandFirst: false, shorthandLast: true } ], 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', 'react/require-default-props': 'off', 'require-jsdoc': 'off', 'sort-keys-fix/sort-keys-fix': 'error' }, settings: { 'import/parsers': { '@typescript-eslint/parser': [ '.ts', '.tsx', '.js' ] }, react: { version: 'detect' } } };
- You can manage ESLint's configuration in .eslintrc.js, and an example is shown above.
- You can add whatever rules you want to rules.
- Refer to Rules - ESLint for the default rules.
- .npmignore
- .npmignore is similar to .gitignore. The difference is that it declares which files to exclude when publishing to npm.
- Files and folders matching the rules in this list are not included when publishing to npm.
TXT
.storybook/ src/ rollup.config.js tsconfig.json yarn.lock
- package.json
- name - when publishing to npm, publishing is done under this name.
- If publishing under an organization, enter it in the form @org/name.
- version - the library version. A version that's already been published cannot be republished, so the version must be managed appropriately with each publish.
- main - the default (CJS) script for this library
- module - the ESM script for this library
- browser - the UMD script for this library
- types - the types for this library
- private - whether it's public on npm
- This is unrelated to the GitHub repository.
- script - the list of the project's script commands
- Add the rollup -c command for building.
- name - when publishing to npm, publishing is done under this name.
JSON
{ "name": "@itcode-dev/react-components-library-starter", "version": "3.0.1", "main": "./dist/index.js", "module": "./dist/index.es.js", "browser": "./dist/index.umd.js", "types": "./dist/index.d.ts", "private": false, "script": { "build": "rollup -c" } }
Publish the library.
BASH
npm login # username # password # email # email otp yarn publish --access public
- Log in with npm login.
- If you don't have an account, create one at the npm homepage.
- Publish with yarn publish --access public.
Let's actually install the published project and try using it.
BASH
npm i @itcode-dev/react-components-library-starter yarn add @itcode-dev/react-components-library-starter
You can install the library with the commands above.
TSX
import Button from '@itcode-dev/react-components-library-starter/dist/atom/Button'; import Input from '@itcode-dev/react-components-library-starter/dist/atom/Input';
You can use the library as shown above.
This project was small in scale, but there was a lot to research. There was a lot to look into despite the small scale, which I think made it fun.
Thanks to this project, I was able to learn a lot of new things.
- Storybook
- classnames
- The npm publishing flow
- rollup.js
I think I picked up a lot of other little things too. It was a rewarding project in many ways.
Come to think of it, I think I published a Java library once before too... unlike npm, I remember Maven's publishing process being complicated.
I'm not sure I could do it again right now if asked to... I should probably write that up again sometime when I have time too.
