DEV Community

Sidharth Mohanty
Sidharth Mohanty

Posted on

How to create and publish a react component library (not the storybook way)

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,

import { RCComponent } from rc-react-component

<RCComponent />
Enter fullscreen mode Exit fullscreen mode

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. But for now, let's create a simple counter component.

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

npm init -y
Enter fullscreen mode Exit fullscreen mode

Install react and react-dom as peer dependencies by,

npm i —save-peer react react-dom
Enter fullscreen mode Exit fullscreen mode

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:

I have also made a separate repository containing configuration files and example libraries created using both rollup and webpack. You can check it 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
Enter fullscreen mode Exit fullscreen mode

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.

// 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(),
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

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",
...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
  ],
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
  },
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  },
}
Enter fullscreen mode Exit fullscreen mode

Lets create the component now,

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

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode
// 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 />);
  });
});
Enter fullscreen mode Exit fullscreen mode
// index.module.css
.red {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

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"
  },
}
Enter fullscreen mode Exit fullscreen mode

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,

// playground/src/App.js
import { SimpleCounterComponent } from "simple-counter-component";
import "./App.css";

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

export default App;

Enter fullscreen mode Exit fullscreen mode

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/ 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"
  ],
Enter fullscreen mode Exit fullscreen mode

Checkout the repository here.

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.

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

Discussion (0)