How to create and publish a react component library (not the storybook way)
Tags: javascript, react, tutorial, beginners
Published: 2022-06-20 14:20:52

Hello everyone! Just some backstory before we start, I got selected for GSoC this year (2022) with Rocket.Chat organization. The project in which I was selected is to create an easy-to-embed React component of Rocket.Chat (like a mini-version of it) that can be plugged into any web application made in React.

Something like this,

```jsx
import { RCComponent } from ‘rc-react-component’

<RCComponent />
```

So when I was writing my proposal, I researched a lot about the ways in which we can create a React component library.

As my project demanded that it should be a single component that should be tightly coupled up with the backend features provided by the RocketChat API, I and my mentor decided to go with a traditional approach of creating a React component library i.e, by not using Storybook.

I wanted to share this way, where you can get started with creating a component library instantly and naturally (without worrying about learning any other technology). For a detailed approach about why I chose some things over the others, I will be writing bi-weekly blogs about my progress in the [EmbeddedChat project](https://github.com/RocketChat/EmbeddedChat). But for now, let's create a [simple counter component](https://github.com/RocketChat/EmbeddedChat).

First of all create a project directory and initialize your npm project with,

```
npm init -y
```

Install react and react-dom as peer dependencies by,

```
npm i —save-peer react react-dom
```

I went with rollup as my bundler of choice but you can go with any bundler of your preference. I am linking some articles that made up my mind about choosing rollup for creating component libraries:

- [Benchmarking different bundlers](https://blog.logrocket.com/benchmarking-bundlers-2020-rollup-parcel-webpack/)
- [webpack vs rollup](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c)

I have also made a separate repository containing configuration files and example libraries created using both rollup and webpack. You can [check it](https://github.com/sidmohanty11/crl-templates) out too if you want to go with webpack.

Now, let's install rollup and all the plugin dependencies

```
npm i —save-dev rollup rollup-plugin-postcss @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-peer-deps-external
```

After installation, lets create a `rollup.config.js` file which will contain our configuration for desired output files. I went with both `cjs` and `esm` modules.

```jsx
// rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
import postcss from "rollup-plugin-postcss";
import external from "rollup-plugin-peer-deps-external";

const packageJson = require("./package.json");

export default [
  {
    input: "src/index.js",
    output: [
      { file: packageJson.main, format: "cjs", sourcemap: true },
      { file: packageJson.module, format: "esm", sourcemap: true },
    ],
    plugins: [
      resolve(),
      commonjs({ include: ['node_modules/**'] }),
      babel({
        exclude: "node_modules/**",
        presets: ["@babel/env", "@babel/preset-react"],
        babelHelpers: 'bundled'
      }),
      postcss(),
      external(),
    ],
  },
];
```

As you can see we are using `packageJson.main` and `packageJson.module` so let's add them,

```
// package.json
{
...
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
...
}
```

Install babel and all the required dependencies to work with React.

```
npm i --save-dev @babel/core @babel/preset-env @babel/preset-react babel-jest
```

Create a `babel.config.js` file and add these,

```
// babel.config.js
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        modules: false,
        bugfixes: true,
        targets: { browsers: "> 0.25%, ie 11, not op_mini all, not dead" },
      },
    ],
    "@babel/preset-react",
  ],
};
```

For testing, I am going with jest and react-testing-library and these can be installed by,

```
npm i --save-dev jest @testing-library/react react-scripts identity-obj-proxy
```

Add the jest configuration file, create `jest.config.js` and add,

```
// jest.config.js
module.exports = {
  testEnvironment: "jsdom",
  moduleNameMapper: {
    ".(css|less|scss)$": "identity-obj-proxy",
  },
};
```

We need `react-scripts` to run tests and to use it inside the playground for running all the scripts (start, build, test and eject) this will ensure we get no conflicts. `identity-obj-proxy` is needed because when we will be running tests, jest cannot determine what we are importing from module CSS, so it will proxy it to an empty object of sorts.

 We will be needing some more dependencies to run our project and use them in our scripts, lets's install them too,

```
npm i --save-dev npm-run-all concurrently cross-env rimraf
```

Let’s add some scripts to run our project now,

```
// package.json
{
"scripts": {
    "prebuild": "rimraf dist",
    "build": "rollup -c",
    "watch": "rollup -c --watch",
    "dev": "concurrently \" npm run watch \" \" npm run start --prefix playground \"",
    "test": "run-s test:unit test:build",
    "test:unit": "cross-env CI=1 react-scripts test --env=jsdom",
    "test:watch": "react-scripts test --env=jsdom --coverage --collectCoverageFrom=src/components/**/*.js",
    "test:build": "run-s build",
    "prepublish": "npm run build"
  },
}
```

Lets create the component now,

Create `src` directory and inside this create `index.js`, `index.test.js`, and `index.module.css`

```jsx
// index.js
import React, { useState } from "react";
import styles from "./index.module.css";

export const SimpleCounterComponent = () => {
  const [counter, setCounter] = useState(0);
  return (
    <div>
      <h1 className={styles.red}>Counter Component</h1>
      <div>{counter}</div>
      <button onClick={() => setCounter((prev) => prev + 1)}>increment</button>
    </div>
  );
};
```

```jsx
// index.test.js
import React from "react";
import { render } from "@testing-library/react";

import { SimpleCounterComponent } from "./index";

describe("SimpleCounterComponent Component", () => {
  test("renders the SimpleCounterComponent component", () => {
    render(<SimpleCounterComponent />);
  });
});
```

```
// index.module.css
.red {
  color: red;
}
```

Now, when you run `npm run build` it will create a `dist` directory with our bundled output files (in both cjs and esm formats) but you definitely need to test your component before you ship, right?

Create a playground app by running `npx create-react-app playground`. Remember we downloaded `react-scripts`, change package.json of the playground app as follows,

```
// playground/package.json
{
	"dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "file:../node_modules/react",
    "react-dom": "file:../node_modules/react-dom",
    "react-scripts": "file:../node_modules/react-scripts",
    "simple-counter-component": "file:../",
    "web-vitals": "^2.1.4"
  },
	"scripts": {
    "start": "node ../node_modules/react-scripts/bin/react-scripts.js start",
    "build": "node ../node_modules/react-scripts/bin/react-scripts.js build",
    "test": "node ../node_modules/react-scripts/bin/react-scripts.js test",
    "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject"
  },
}
```

This will make use of the react-scripts downloaded in the root and also point to use react, react-dom that’s installed in the root. This will save you from 3 days of headache if you are not familiar with how `npm link` works, and will throw an error that different `react` versions are used in your project and hooks cannot be used etc. 

Now do an `npm install` in the playground, and you are ready to go.

Use your component inside the playground,
```jsx
// playground/src/App.js
import { SimpleCounterComponent } from "simple-counter-component";
import "./App.css";

function App() {
  return (
    <div className="App">
      <SimpleCounterComponent />
    </div>
  );
}

export default App;

```

Go back to the root directory and run `npm run dev` it will open up the playground application and you can do your changes in the component while watching the changes reflect real-time in the playground environment.

Now for publishing your component, make sure you use a name that has not been taken yet. After you come up with a name, you can use it in `package.json`'s `name` attribute.

You can just do `npm publish` to publish your package, but it can show you an error if this is your first time. You need to create an account in [https://www.npmjs.com/](https://www.npmjs.com/) and after that login using `npm login` in your terminal. After you’ve successfully logged in yourself, `npm publish`!

You can further improve your project by adding ESlint, prettier, terser-plugin (to minify) etc. which I am not including in this blog.

Last important thing, make sure you are shipping only the required module and not everything. This will heavily determine the size of your package. So if you want to just ship the `dist` directory, add this in your `package.json`.
```
// package.json
 "files": [
    "dist"
  ],
```

Checkout the repository [here](https://github.com/sidmohanty11/simple-counter-component).

Hooray! Our package has been published. You can do `npm i simple-counter-component` to check it out. To manage semantic versioning, you can use a great library called [np](https://www.npmjs.com/package/np).

Please let me know the things that can be improved in the comment section below. Thank you.

If you want to connect:
Email : sidmohanty11@gmail.com
GitHub: https://github.com/sidmohanty11
LinkedIn: https://www.linkedin.com/in/sidmohanty11
Twitter: https://twitter.com/sidmohanty11