How to create and publish a react component library (not the storybook way)
Sidharth Mohanty
Posted on June 20, 2022
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 />
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
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:
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
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(),
],
},
];
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
// 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>
);
};
// 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,
// 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/ 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.
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
Posted on June 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 7, 2022