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
- 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
- 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)
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
.
- Create a
.gitignore
file, to excludenode_modules
and other files later on
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
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
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
}
}
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;
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";
2nd export: src/components/index.ts
export { default as Button } from "./Button";
3rd export: src/index.ts
export * from './components';
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
step 2:
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.
@rollup/plugin-typescript
: Needstslib
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()],
},
];
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"
}
- "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
You will notice a new folder appear, named dist
Publishing our Library to npm
Create an npm account, ignore if you already have one
In root of your project directory, run
npm login
-
Update your
package.json
to include correct information of the name, version and description.keep the version number 0.0.1 initially
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
- Let us use our
<Button/>
component inApp.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;
- Save it and restart the app, we see our Component working as intended.
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 theButton
directorysrc/components/Button/button.css
button.css
.btn{
background-color: blueviolet;
}
- Use the
btn
class in ourButton.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;
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
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
},
];
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
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
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)$/],
},
];
Run
npm run rollup-build-lib
to create updated distUpdate 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
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"
}
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
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
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π¦
Top comments (11)
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.
Nice article, I am getting issue with the command
npm run rollup-build-lib
throws below errorDo you have any idea
github.com/rollup/rollup/issues/3594
One answer might be to try and check if your node is updated or not.
I have removed this line
import packageJson from "./package.json" assert { type: "json" };
and provided
cjs
andesm
path directly in the file field, just to make it work.Just curious to know is this good pratice?
Its different issue, may i know your node version ?
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 beenrollup -c
, corrected nowMy 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.
No worry, issue has been resolved same day
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...
Wow.... it was a really great read. I really needed this.ππ€©
Much needed one β€οΈ Just built this UI library for my company following this article: npmjs.com/package/@teamartizen/rea...
pure gold. many thanks!