Build and publish a Component Library - React, TypeScript, Storybook
Tags: componentlibrary, typescript, react, storybook
Published: 2023-02-26 14:35:09

## 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

```bash 
mkdir abhi-cl-blog  -> cd abhi-cl-blog
```

- Initialize the project 

```
npm init
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxenw52klxm07jab1be4.png)


- Initialize git 
```
git init
```

> 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
```
--save-dev installs them as `devDependency` ([read more](https://sentry.io/answers/npm-difference-between-dependencies/))

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gg27tdpj9v7lgwn7r6an.jpg)

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

![gitignore file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwgyqx1we4j8yhlx7bg4.png)


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

```js
-
├── src
│   ├── components
|   │   ├── Button
|   |   │   ├── Button.tsx
|   |   │   └── index.ts
|   │   └── index.ts
│   └── index.ts
├── package.json
└── package-lock.json
```

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](https://medium.com/bootstart/you-should-be-using-folder-components-b30b7d165c39))

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

- Initialize and configure TypeScript
```js
npx tsc --init
```

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.

```js
{
  "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

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

What each do can be viewed in my [gist](https://gist.github.com/abhijitdotsharma/b9cfefcaec85ead96abf3a633df9d006)

And if you want to learn [more](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) about tsconfig


- Build Button.tsx inside `src/components/Button`.

`Button.tsx`

```ts
import React from "react";


export interface ButtonProps{
    label: string;
		
}

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

export default Button;

```
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 <Button /> 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`
```js
// This is importing Button and exporting it directly
// Syntactic sugar
export { default } from "./Button";
```

2nd export: `src/components/index.ts`
```js
export { default as Button } from "./Button";
```

3rd export: `src/index.ts`
```js
export * from './components';
```

If you want to compare, check out this [commit](https://github.com/abhijitdotsharma/abhi-cl-blog/commit/08a2f6980749b7cefe7c347e9a9da80fd24a9b23) to compare your files

Want to learn more about the above exports? Check this [answer](https://stackoverflow.com/a/39999593) 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:  
```js
npm install --save-dev tslib
```

step 2:
```js
npm install rollup @rollup/plugin-node-resolve 
@rollup/plugin-typescript @rollup/plugin-commonjs 
rollup-plugin-dts --save-dev
```

- `@rollup/plugin-node-resolve` : Allows Rollup to resolve dependencies of the library, making it possible to import modules from external packages. 
<br>

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

- `@rollup/plugin-commonjs` : Convert CommonJS modules to ES6.
<br>

- `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.

<br>

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

`rollup.config.mjs`

```js
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()],
    },

];
```

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`

```js
//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"
}
```

- "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:
```js
npm run rollup-build-lib
```

You will notice a new folder appear, named dist

![Dist folder image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3hosxpnduc7rd72wrvm.png)


---
### 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](https://www.youtube.com/watch?v=J4b_T-qH3BY&t=375s)

--- 


### Testing our Library in a project
- Create a new React app(using CRA, Vite...)

- Open the new app

- Install your library from npm
```js
npm install <YOUR_PACKAGE_NAME>
// npm install abhi-cl-blog
```
- Let us use our `<Button/>` component in `App.tsx`

```js
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;

```

- Save it and restart the app, we see our Component working as intended.


![App using Component from Our Library](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ydgfn5gp2tfkfx09wyjd.png)




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`
```css
.btn{
   background-color: blueviolet;
}
```

- Use the `btn` class in our `Button.tsx`. 

```ts
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;

```
<br>

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.

```js
npm install postcss rollup-plugin-postcss —save-dev
```
`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).

<br>

Update our rollup config

`rollup.config.mjs`
```js
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
    },

];

```

Now we can republish (or update) our package.
<br>
- update the version number in `package.json` to 0.0.2
```js
npm run rollup-build-lib
num publish
```

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.

```js
npm install --save-dev @rollup/plugin-terser rollup-plugin-peer-deps-external
```

With this installed, we update our rollup config.

`rollup.config.mjs`

```js
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)$/],
    },
  ];

```

- 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,
```js
npx storybook init
```
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`

```ts
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"
}

```

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](https://storybook.js.org/docs/react/writing-stories/introduction)

Let us run storybook
```js
npm run storybook
```

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i2zvrbwxhdks35yaqa2p.png)

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 :
- [Rest and Spread](https://dev.to/abhijitdotsharma/javascript-spread-and-rest-operators-an-essential-guide-for-beginners-53fc)

You can follow me on [LinkedIn](https://www.linkedin.com/in/abhijitdotsharma/), [Twitter🐦](https://twitter.com/abhi_exe)