DEV Community

Cover image for Build and publish a Component Library - React, TypeScript, Storybook
abhijitdotsharma
abhijitdotsharma

Posted on • Updated on

Build and publish a Component Library - React, TypeScript, Storybook

Introduction

At the end of this article, you will create your own custom React Component Library, and publish it to npm which will let others use it via a simple npm install.

Why?

React took over modern web development due to its component driven architecture.

Components can be the smallest atomic part of a huge application, and we tend to reuse them often. For example, a Button is used everywhere, Login page, Signup page, for CTA (Call to Action) to name a few.

These reusable components make up Pages which in turn make up an Application. Having a component library, has many advantages

  • Consistent styling
  • Speed of development
  • Maintainability

Today, we will learn how modern Component Libraries (like Chakra UI, Material UI) can be built and used by us for our projects.


Tools and Knowledge Required

  • VS Code (or any Code editor you prefer)
  • NPM
  • Git
  • React
  • TypeScript
  • Storybook

Let's start building πŸ› οΈ

Building a skeleton 🩻

  • Make an empty directory or cd into an existing one
mkdir abhi-cl-blog  -> cd abhi-cl-blog
Enter fullscreen mode Exit fullscreen mode
  • Initialize the project
npm init
Enter fullscreen mode Exit fullscreen mode

This will create a package.json, just click enter and move ahead as we will edit them later on. The image below will be similar to the package.json generated for you

package.json

  • Initialize git
git init
Enter fullscreen mode Exit fullscreen mode

Make atomic commits when learning/building for the first time, it’ll make use go back/realize the step at which the error happened

  • Install React and TypeScript to get started
npm install react react-dom typescript @types/react --save-dev
Enter fullscreen mode Exit fullscreen mode

--save-dev installs them as devDependency (read more)

Note: Since we will be publishing this library to npm for others to use, we will have to ensure that users when using our library have the correct version of dependencies, so we save react and react-dom as peerDependencies.

package.json till now

  • Create a .gitignore file, to exclude node_modules and other files later on

gitignore file

Create a commit at this stage, you can revert back to this stage if you get some errors later, instead of yelling at your pc and restarting the tutorial πŸ™‚


Building our first component

To create our component, make the following structure

-
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ components
|   β”‚   β”œβ”€β”€ Button
|   |   β”‚   β”œβ”€β”€ Button.tsx
|   |   β”‚   └── index.ts
|   β”‚   └── index.ts
β”‚   └── index.ts
β”œβ”€β”€ package.json
└── package-lock.json
Enter fullscreen mode Exit fullscreen mode

We are building a library and want it to be easy for users to consume/import our components, hence we are creating index files at each level (read more here)

There are three index.ts files, double check it before moving ahead.

  • Initialize and configure TypeScript
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

This will create a tsconfig.json file in the root of our project, and it has default configurations for TypeScript, some of which we will modify.

{
  "compilerOptions": {
     "target": "es2016",
     "jsx": "react",
     "module": "ESNext",
     "moduleResolution": "node",
     "declaration": true, 
     "emitDeclarationOnly": true,
     "outDir": "dist", 
     "declarationDir": "types",
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
     "strict": true, 
     "skipLibCheck": true

  }
}
Enter fullscreen mode Exit fullscreen mode

This will be our tsconfig.json file, copy it in your project

What each do can be viewed in my gist

And if you want to learn more about tsconfig

  • Build Button.tsx inside src/components/Button.

Button.tsx

import React from "react";


export interface ButtonProps{
    label: string;

}

const Button = ( {label}: ButtonProps) => {
    return <button>{label}</button>
}

export default Button;

Enter fullscreen mode Exit fullscreen mode

Here we are defining an interface for the props that our Button will receive.

And then we are building a simple <Button /> component that takes label as a prop and returns a html button element with the passed label prop.

We are going to publish our Library with a single component, and confirm that it works, then we can add more components as we like later on.

We will now export our Button.

1st export: src/components/Button/index.ts

// This is importing Button and exporting it directly
// Syntactic sugar
export { default } from "./Button";
Enter fullscreen mode Exit fullscreen mode

2nd export: src/components/index.ts

export { default as Button } from "./Button";
Enter fullscreen mode Exit fullscreen mode

3rd export: src/index.ts

export * from './components';
Enter fullscreen mode Exit fullscreen mode

If you want to compare, check out this commit to compare your files

Want to learn more about the above exports? Check this answer on StackOverflow


Adding Rollup

Rollup is a tool similar to webpack, and we will be using it to bundle our library to then publish to npm.

Note: Before we start with this process, it is important to remember that these bundling tools use a lot of packages, which get updated frequently so it’s possible that you might run into errors.
I will try to explain what each install does, so you can try to fix if you are stuck. And you can also comment here if you find something, and I’ll try to fix it.

step 1:

npm install --save-dev tslib
Enter fullscreen mode Exit fullscreen mode

step 2:

npm install rollup @rollup/plugin-node-resolve 
@rollup/plugin-typescript @rollup/plugin-commonjs 
rollup-plugin-dts --save-dev
Enter fullscreen mode Exit fullscreen mode
  • @rollup/plugin-node-resolve : Allows Rollup to resolve dependencies of the library, making it possible to import modules from external packages.

  • @rollup/plugin-typescript : Needs tslib as a peer dependency hence, step 1. Used to transpile TypeScript code in the library.

  • @rollup/plugin-commonjs : Convert CommonJS modules to ES6.

  • rollup-plugin-dts : Used to generate a .d.ts file that provides TypeScript type definitions for the library. This is important for TypeScript users, as it allows them to use the library with full type safety.

We will now create a configuration file in our project root.

rollup.config.mjs

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";

import packageJson from "./package.json" assert { type: "json" };


export default [
    {
        input: "src/index.ts",
        output: [
            {
                file: packageJson.main,
                format: "cjs",

            },
            {
                file: packageJson.module,
                format: "esm",

            },
        ],
        plugins: [
            resolve(),
            commonjs(),
            typescript({tsconfig: "./tsconfig.json"}),

        ],
    },
    {
        input: "dist/esm/types/index.d.ts",
        output: [{ file: "dist/index.d.ts", format: "esm" }],
        plugins: [dts()],
    },

];
Enter fullscreen mode Exit fullscreen mode

The above code block is a Rollup configuration file, which is used to bundle a React component library created with Typescript.

First Configuration Object
input is our entry point for our library, i.e. index.ts file in the src directory which exports all of our components.

We use both ESM and commonJS modules to distribute our library so users can choose which type to consume.

The 3 plugins that we are invoking, determine the actual JavaScript code generated

Second Configuration Object
It determines how types in our library are distributed and it uses dts plugin to do it.

We will now update main and module in our package.json

//package.json
{
  "name": "abhi-cl-blog", // πŸ‘ˆname it what you want
  "version": "0.0.1", 
  "description": "A Component Library for Building React Applications faster",
  "scripts": {
// πŸ‘‡πŸ‘‡This is what you will run to create a library
    "rollup-build-lib": "rollup -c"
  },
  "author": "Abhijit Sharma",
  "license": "ISC",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.0.1",
    "@rollup/plugin-node-resolve": "^15.0.1",
    "@rollup/plugin-typescript": "^11.0.0",
    "@types/react": "^18.0.27",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rollup": "^3.14.0",
    "rollup-plugin-dts": "^5.1.1",
    "tslib": "^2.5.0",
    "typescript": "^4.9.5"
  },
 "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
//new additions πŸ‘‡πŸ‘‡
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "files": [
    "dist"
  ],
  "types": "dist/index.d.ts"
}
Enter fullscreen mode Exit fullscreen mode
  • "main" - Output path for commonJS modules.
  • "module" - Output path for es6 modules.
  • "files" - We have defined the output directory for our entire library.
  • "types" - We have defined the location for our library's types.
  • "scripts" - we will use this to run our scripts. eg: npm run rollup-build-lib

Run the rollup script:

npm run rollup-build-lib
Enter fullscreen mode Exit fullscreen mode

You will notice a new folder appear, named dist

Dist folder image


Publishing our Library to npm

  1. Create an npm account, ignore if you already have one

  2. In root of your project directory, run npm login

  3. Update your package.json to include correct information of the name, version and description.

    keep the version number 0.0.1 initially

  4. Run npm publish

CongratulationsπŸ₯³, you just published your component library to npm.

If you are not able to, there are multiple tutorials, and videos that explain this. This one is great yt


Testing our Library in a project

  • Create a new React app(using CRA, Vite...)

  • Open the new app

  • Install your library from npm

npm install <YOUR_PACKAGE_NAME>
// npm install abhi-cl-blog
Enter fullscreen mode Exit fullscreen mode
  • Let us use our <Button/> component in App.tsx
import React from "react";
import { Button } from "YOUR_PACKAGE_NAME";
// import {Button} from "abhi-cl-blog";

function App() {
  return <Button label="Building Stuff is fun"/>;
}

export default App;

Enter fullscreen mode Exit fullscreen mode
  • Save it and restart the app, we see our Component working as intended.

App using Component from Our Library

Pat yourself! You just built a working component library, which can now be used by everyone πŸ™Œ

You can now leave the tutorial if you want to continue yourself, as the next part will involve us learning how to add

  • CSS
  • Storybook

Adding CSS

If we want our components to have some styling, we have to use CSS.

  • Create a button.css file inside the Button directory src/components/Button/button.css

button.css

.btn{
   background-color: blueviolet;
}
Enter fullscreen mode Exit fullscreen mode
  • Use the btn class in our Button.tsx.
import React from "react";
import "./button.css" // πŸ‘ˆnew addition

export interface ButtonProps{
    label: string;

}

const Button = ({label}: ButtonProps) => {
    // btn class added πŸ‘‡πŸ‘‡
    return <button className="btn">{label}</button>
}

export default Button;

Enter fullscreen mode Exit fullscreen mode

This seems like it will work, but the import "./button.css" will not be understood by Rollup, hence won't be used. We have to add some more configurations to make Rollup understand how to process what we are writing.

npm install postcss rollup-plugin-postcss β€”save-dev
Enter fullscreen mode Exit fullscreen mode

rollup-plugin-postcss is used to bundle the CSS files into the final build, And postcss itself is used to transform the CSS to make it compatible with different browsers (will be used when we work with tailwind).

Update our rollup config

rollup.config.mjs

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import packageJson from "./package.json" assert { type: "json" };

import postcss from "rollup-plugin-postcss"; //πŸ‘ˆ new

export default [
    {
        input: "src/index.ts",
        output: [
            {
                file: packageJson.main,
                format: "cjs",

            },
            {
                file: packageJson.module,
                format: "esm",

            },
        ],
        plugins: [
            resolve(),
            commonjs(),
            typescript({tsconfig: "./tsconfig.json"}),
          // πŸ‘‡ new
            postcss({
                plugins: []
              })
        ],
    },
    {
        input: "dist/esm/types/index.d.ts",
        output: [{ file: "dist/index.d.ts", format: "esm" }],
        plugins: [dts()],
        external: [/\.(css|less|scss)$/], //πŸ‘ˆ new
    },

];

Enter fullscreen mode Exit fullscreen mode

Now we can republish (or update) our package.

  • update the version number in package.json to 0.0.2
npm run rollup-build-lib
num publish
Enter fullscreen mode Exit fullscreen mode

Test this in your demo-app again, to see that the css is now being applied.

Optimize using terser

This is an optional step, just to make our bundle size smaller.

npm install --save-dev @rollup/plugin-terser rollup-plugin-peer-deps-external
Enter fullscreen mode Exit fullscreen mode

With this installed, we update our rollup config.

rollup.config.mjs

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import packageJson from "./package.json" assert { type: "json" };

import postcss from "rollup-plugin-postcss";

// πŸ‘‡new imports
import terser from "@rollup/plugin-terser";
import peerDepsExternal from "rollup-plugin-peer-deps-external";

export default [
    {
      input: "src/index.ts",
      output: [
        {
          file: packageJson.main,
          format: "cjs", 
        },
        {
          file: packageJson.module,
          format: "esm",
        },
      ],
      plugins: [
        peerDepsExternal(), // πŸ‘ˆ new line
        resolve(),
        commonjs(),
        typescript({ tsconfig: "./tsconfig.json" }),
        postcss({
          plugins: []
        }),
        terser(), // πŸ‘ˆ new line
      ],
    },
    {
      input: "dist/esm/types/index.d.ts",
      output: [{ file: "dist/index.d.ts", format: "esm" }],
      plugins: [dts()],
      external: [/\.(css|less|scss)$/],
    },
  ];

Enter fullscreen mode Exit fullscreen mode
  • Run npm run rollup-build-lib to create updated dist

  • Update the version number in package.json

  • Run npm publish to update the library

Integrating Storybook

Storybook is a powerful tool for developing and testing components in isolation. It allows to build and view your components in a sandbox environment, without having to worry about the rest of your application. This makes it much easier to iterate on your components and ensure that they are working correctly before integrating them into your larger application.

Additionally, Storybook provides a great way to document your components, making it easy for other developers to understand how to use them. Overall, Storybook is an essential tool for any developer building a component library or working with reusable Ul components.

In essence, storybook will let us test our Button without us having to create a react app.

In the root of your project run,

npx storybook init
Enter fullscreen mode Exit fullscreen mode

Storybook will detect our project is in react, how? (Google)

You will notice few new folders created by storybook .storybook and src/stories.
Delete the src/stories directory as we will learn how to create our own stories.

Let's create our story, create a file in the src/components/Button directory named Button.stories.tsx

src/components/Button/Button.stories.tsx

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import Button from './Button';

// You can learn about this: https://storybook.js.org/docs/react/writing-stories/introduction

export default { 
    title: 'Button',
    component: Button,
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />

export const Primary = Template.bind({});
Primary.args = {
    label: "Primary"
}

export const Secondary = Template.bind({})
Secondary.args = {
    label: "Secondary"
}

Enter fullscreen mode Exit fullscreen mode

There are two basic levels of organization in Storybook:
The component and its child stories. Think of each story as a permutation of a component.
You can have as many stories per component as you need.

  • Component (Button)
  • Story (Primary Button)
  • Story (Secondary Button)
  • Story (Large Button)

export default defines our button that will appear in Storybook

Template and Template.bind is a really great concept, which you can have a look here

Let us run storybook

npm run storybook
Enter fullscreen mode Exit fullscreen mode

If you get some errors, don't stress and try to read through the error and try to fix it as these tools get updated frequently.

If it runs well, you will see this

Storybook app

This is just the beginning, and you would love it if you read more about storybook through its documentation (they even have some great yt videos).

Final Thoughts

Great job on going through this article, this article exposes you to a lot of new concepts like

  • Playing with config files
  • Bundling you own library
  • Publishing to npm
  • Making atomic commits
  • Storybook

These are all great learning experiences, so congratulations πŸ₯³.

Now, you are ready to build your own component library, and publish it to the world.

If you liked this article and you think this will help other's out there, feel free to share it. Comment if you feel something can be improved or added.


If you like to read more :

You can follow me on LinkedIn, Twitter🐦

Latest comments (11)

Collapse
 
kaptn3 profile image
Victoria Kapitonenko

Thank you very much for this tutorial! I tried to create cra-template with your configurations, maybe it will be convenient for someone github.com/rebase-agency/cra-templ...

Collapse
 
shaheem_mpm profile image
Mohammed Shaheem P

Much needed one ❀️ Just built this UI library for my company following this article: npmjs.com/package/@teamartizen/rea...

Collapse
 
warlyware profile image
Dan Ward

pure gold. many thanks!

Collapse
 
hidaytrahman profile image
Hidayt Rahman

Nice article, I am getting issue with the command npm run rollup-build-lib throws below error

[!] SyntaxError: Unexpected identifier
    at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:139:18)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:236:14)
    at async link (node:internal/modules/esm/module_job:67:21)
Enter fullscreen mode Exit fullscreen mode

Do you have any idea

Collapse
 
abhijitdotsharma profile image
abhijitdotsharma

github.com/rollup/rollup/issues/3594

One answer might be to try and check if your node is updated or not.

Collapse
 
hidaytrahman profile image
Hidayt Rahman • Edited

I have removed this line

import packageJson from "./package.json" assert { type: "json" };

and providedcjs and esm path directly in the file field, just to make it work.

Just curious to know is this good pratice?

Collapse
 
hidaytrahman profile image
Hidayt Rahman

Its different issue, may i know your node version ?

Thread Thread
 
abhijitdotsharma profile image
abhijitdotsharma

Hi Hidayat while I did not fully understand how you got that error, I am grateful you brought it up as I tried to test my article today by creating a new Component Library and I noticed that I had made 2-3 typo's which when I removed, resulted in correct compilation of the library.

rollup-lib -c should have been rollup -c, corrected now

My current node version is 18.14.2 LTS and I think if you try it once again it will work. Thank you for your comment once again and hit me back if that doesn't work.

Thread Thread
 
hidaytrahman profile image
Hidayt Rahman

No worry, issue has been resolved same day

Collapse
 
gabrielmlinassi profile image
Gabriel Linassi

In my company we do have a set of components we share amongst multiple projects and we use TurboRepo monorepo for that. We thought about creating another repo but mono ended up being a much better option.

Collapse
 
nityam0213 profile image
Nityam

Wow.... it was a really great read. I really needed this.πŸ‘πŸ€©