Ashish Kachhadiya · Follow
Published in · 15 min read · Sep 22, 2023
--
A thorough tutorial on creating your own React component libraries and sharing them across projects via GitHub and the npm repository.
React is one of the most famous libraries for making scalable web applications. The React ecosystem has many component libraries like Ant Design, Material UI, and Chakra UI, which provide reusable UI components.
With the flexibility of React, you can also build custom component libraries suited to their specific needs. In this article, we’ll learn about how to make your own component library with React, Typescript, and Storybook, along with a few other useful tools.
To initialize the project with git along with React and Typescript, run the following commands:
git init
npm init -y
npm install -D react @types/react typescript
Here, we need to move react
to peerDependencies
because it’s typically used as a peer dependency in library packages. This allows consumers to use one version of React without conflicts. To do this, add the following lines to your package.json
and remove React from devDependencies:
"peerDependencies": {
"react": "^18.2.0"
},
Prettier
Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules.
To install Prettier, run the following command:
npm install -D prettier
Create .prettierrc
at the root of the project and set rules as follows:
{
"printWidth": 80,
"tabWidth": 2
}
To format the project, add the following script in package.json
:
{
...
"scripts": {
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'"
},
...
}
ESLint
ESLint is a static code analysis tool that checks your JavaScript code for common problems, such as syntax errors, formatting issues, code style violations, and potential bugs.
To install the ESLint with its plugin, run the following command:
npm install -D eslint @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-storybook @typescript-eslint/eslint-plugin
Now, create a config file for ESLint named .eslintrc
at the root of the project and paste the following configuration:
{
// Specify the environments where the code will run
"env": {
"jest": true, // Enable Jest for testing
"browser": true // Enable browser environment
}, // Stop ESLint from searching for configuration in parent folders
"root": true,
// Specify the parser for TypeScript (using @typescript-eslint/parser)
"parser": "@typescript-eslint/parser", // Leverages TS ESTree to lint TypeScript
// Add additional rules and configuration options
"plugins": ["@typescript-eslint"],
// Extend various ESLint configurations and plugins
"extends": [
"eslint:recommended", // ESLint recommended rules
"plugin:react/recommended", // React recommended rules
"plugin:@typescript-eslint/recommended", // TypeScript recommended rules
"plugin:@typescript-eslint/eslint-recommended", // ESLint overrides for TypeScript
"prettier", // Prettier rules
"plugin:prettier/recommended", // Prettier plugin integration
"plugin:react-hooks/recommended", // Recommended rules for React hooks
"plugin:storybook/recommended" // Recommended rules for Storybook
],
"rules": {
"react/react-in-jsx-scope": "off",
}
}
Create a .gitignore
file in the root directory and add directories that we don’t have to include in the repository.
node_modules
dist#storybook build directory
storybook-static
For linting the project, add the following script in package.json
:
{
...
"scripts": {
"lint": "eslint . --ext .ts,.tsx --ignore-path .gitignore --fix"
},
...
}
Vite, a modern front-end tool, has become popular in recent years. While it uses Rollup for production builds, it combines the strengths of both tools to offer a great development experience and efficient production builds.
Create a file named tsconfig.json
and paste the following configuration:
{
"compilerOptions": {
"target": "ES5", // Specifies the JavaScript version to target when transpiling code.
"useDefineForClassFields": true, // Enables the use of 'define' for class fields.
"lib": ["ES2020", "DOM", "DOM.Iterable"], // Specifies the libraries available for the code.
"module": "ESNext", // Defines the module system to use for code generation.
"skipLibCheck": true, // Skips type checking of declaration files. /* Bundler mode */
"moduleResolution": "bundler", // Specifies how modules are resolved when bundling.
"allowImportingTsExtensions": true, // Allows importing TypeScript files with extensions.
"resolveJsonModule": true, // Enables importing JSON modules.
"isolatedModules": true, // Ensures each file is treated as a separate module.
"noEmit": true, // Prevents TypeScript from emitting output files.
"jsx": "react-jsx", // Configures JSX support for React.
/* Linting */
"strict": true, // Enables strict type checking.
"noUnusedLocals": true, // Flags unused local variables.
"noUnusedParameters": true, // Flags unused function parameters.
"noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement.
"declaration": true, // Generates declaration files for TypeScript.
},
"include": ["src"], // Specifies the directory to include when searching for TypeScript files.
"exclude": [
"src/**/__docs__","src/**/__test__"
]
}
To install Vite with one plugin that generates a declaration file, run the following command:
npm install -D vite vite-plugin-dts
Create a file named vite.config.ts
in the root directory with the following configuration:
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import { peerDependencies } from "./package.json";export default defineConfig({
build: {
lib: {
entry: "./src/index.ts", // Specifies the entry point for building the library.
name: "vite-react-ts-button", // Sets the name of the generated library.
fileName: (format) => `index.${format}.js`, // Generates the output file name based on the format.
formats: ["cjs", "es"], // Specifies the output formats (CommonJS and ES modules).
},
rollupOptions: {
external: [...Object.keys(peerDependencies)], // Defines external dependencies for Rollup bundling.
},
sourcemap: true, // Generates source maps for debugging.
emptyOutDir: true, // Clears the output directory before building.
},
plugins: [dts()], // Uses the 'vite-plugin-dts' plugin for generating TypeScript declaration files (d.ts).
});
Add this configuration to package.json
as it defines the entry points and types definitions with the build script.
{
...
"type": "module",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts":{
...
"build": "tsc && vite build",
}
}
Here, "main"
and "module"
are used to specify the entry points for different module systems in JavaScript.
- The
"main"
field is used to specify the entry point for CommonJS modules. It typically points to a file with the extension.cjs.js
or.js
. When a package is required usingrequire()
in Node.js or bundled with tools like Webpack or Vite, the"main"
entry point is used. - The
"module"
field is used to specify the entry point for ES modules. It typically points to a file with the extension.es.js
or.mjs
. When a package is imported usingimport
in modern JavaScript environments that support ES modules, the"module"
entry point is used.
By specifying both "main"
and "module"
fields in the package.json, we can provide compatibility for both CommonJS and ES module systems.
Instead of using simple CSS, we are going to use styled components for styling to benefit from features like component-based styling, dynamic styling, and css-in-js.
To add styled-components, run the following command.
npm install -D styled-components
Create a src
folder in the root directory and then, create a folder named button
for our button components. Add Button.tsx
and index.ts
in this folder and paste the following code:
// components/button/button.tsx
import React, { MouseEventHandler } from "react";
import styled from "styled-components";export type ButtonProps = {
text?: string;
primary?: boolean;
disabled?: boolean;
size?: "small" | "medium" | "large";
onClick?: MouseEventHandler<HTMLButtonElement>;
};
const StyledButton = styled.button<ButtonProps>`
border: 0;
line-height: 1;
font-size: 15px;
cursor: pointer;
font-weight: 700;
font-weight: bold;
border-radius: 10px;
display: inline-block;
color: ${(props) => (props.primary ? "#fff" : "#000")};
background-color: ${(props) => (props.primary ? "#FF5655" : "#f4c4c4")};
padding: ${(props) =>
props.size === "small"
? "7px 25px 8px"
: props.size === "medium"
? "9px 30px 11px"
: "14px 30px 16px"};
`;
const Button: React.FC<ButtonProps> = ({
size,
primary,
disabled,
text,
onClick,
...props
}) => {
return (
<StyledButton
type="button"
onClick={onClick}
primary={primary}
disabled={disabled}
size={size}
{...props}
>
{text}
</StyledButton>
);
};
export default Button;
// components/button/index.ts
export { default as Button } from './Button';
Add an index.ts
file to the components
folder, as this file will allow you to export all the components from the components
folder.
// components/index.ts
export * from './button'; // Add more exports for other components as needed
Add an index.ts
file to the src
folder as it serves as the entry point for your entire library. From here, you can export components along with their types and utilities.
// src/index.ts
export * from './components'; // This will export all components from the 'components' folder
After adding a component, run the following command. It generates a ‘dist’ folder.
npm run build
In the dist
folder, you will find the output code for your library.
Vitest is the unit testing framework built on top of Vite and is an excellent unit test framework with many modern features.
To install Vitest, run the following command:
npm install -D vitest @testing-library/react jsdom @testing-library/jest-dom
Now, add the following script to thepackage.json
file.
"scripts": {
"test": "vitest run",
"test-watch": "vitest",
"test:ui": "vitest --ui"
}
Add the following line at the top of the vite.config.ts
file:
/// <reference types="vitest" />
Create a file namedsetupTests.ts
in the root directory and add the following code to that file:
import { expect } from "vitest";
import * as matchers from "@testing-library/jest-dom/matchers";
import { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
declare module "vitest" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface Assertion<T = any>
extends jest.Matchers<void, T>,
TestingLibraryMatchers<T, void> {}
}
expect.extend(matchers);
Now, add the following configuration to vite.config.ts
file under defineConfig:
test: {
globals: true,
environment: "jsdom",
setupFiles: "./setupTests.ts",
},
Create __test__
directory in the button folder and add a file named Button.test.tsx
to test the button component with the following code:
//Button/__test__/Button.test.tsx
import React from "react";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Button from "../Button";describe("Button component", () => {
it("Button should render correctly", () => {
render(<Button />);
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
});
});
Storybook
Storybook is an open-source development environment for designing, testing, and showcasing UI components in isolation.
Run the following command to initialize a new storybook project:
npx storybook@latest init
As we already have Vite, it will be detected as a runner in Storybook, and it will also add the .storybook
folder along with the required script in the package.json
file.
It will also generate a stories
folder in the src
folder, but we are going to delete it.
Each component has its __docs__
own directory, and to that, we will add our stories. To do that, we have to update the stories
field in the .stroybook/main.ts
file.
stories: ["../src/**/__docs__/*.stories.tsx", "../src/**/__docs__/*.mdx"],
Create three files in the src/button/__docs__
directory:
Button.stories.tsx
Example.tsx
Button.mdx
Add the following content to Button.mdx.
:
import { Canvas, Meta } from "@storybook/blocks";
import Example from "./Example.tsx";
import * as Button from "./Button.stories.tsx";<Meta of={Button} title="Button" />
# Button
Button component with different props.
#### Example
<Canvas of={Button.Primary} />
## Usage
```ts
import {Button} from "sld-ui";
const Example = () => {
return (
<Button
size={"small"}
text={"Button"}
onClick={()=> console.log("Clicked")}
primary
/>
);
};
export default Example;
```
#### Arguments
- **text** _`() => void`_ - A string that represents the text content of the button.
- **primary** - A boolean indicating whether the button should have a primary styling or not. Typically, a primary button stands out as the main action in a user interface.
- **disabled** - A boolean indicating whether the button should be disabled or not. When disabled, the button cannot be clicked or interacted with.
- **size** - A string with one of three possible values: "small," "medium," or "large." It defines the size or dimensions of the button.
- **onClick** - A function that is called when the button is clicked. It receives a MouseEventHandler for handling the click event on the button element.
In the Example.tsx
file, insert the following code:
import React, { FC } from "react";
import Button, { ButtonProps } from "../Button";const Example: FC<ButtonProps> = ({
disabled = false,
onClick = () => {},
primary = true,
size = "small",
text = "Button",
}) => {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Button
size={size}
text={text}
disabled={disabled}
onClick={onClick}
primary={primary}
/>
</div>
);
};
export default Example;
In the Button.stories.tsx
file, insert the following code:
import type { Meta, StoryObj } from "@storybook/react";
import Example from "./Example";const meta: Meta<typeof Example> = {
title: "Button",
component: Example,
};
export default meta;
type Story = StoryObj<typeof Example>;
export const Primary: Story = {
args: {
text: "Button",
primary: true,
disabled: false,
size: "small",
onClick: () => console.log("Button"),
},
};
export const Secondary: Story = {
args: {
text: "Button",
primary: false,
disabled: false,
size: "small",
onClick: () => console.log("Button"),
},
};
After making these changes, run the following command to start Storybook:
npm run storybook
You should be able to see all the Button UI variations in Storybook.
Husky
Husky is primarily designed to enforce pre-commit hooks in your Git repository, ensuring that certain tasks like running tests, formatting code, and linting are performed before any commits are allowed.
To configure Husky with pre-commit hooks, run the following commands:
npm install -D husky lint-staged
npx husky install
Now, inside the .husky
folder, create a file named pre-commit
hook and add the following to it:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"npx lint-staged
Add the following configuration to thepackage.json
file:
"lint-staged": {
"*.{ts,tsx}": [
"npm run format",
"npm run lint",
"npm run test"
]
}
Now, when we commit our changes using git commit, Husky will automatically run lint-staged, which, in turn, will run our specified formatting and linting scripts on the staged files.
Publishing on GitHub
To your project’s package.json
, add a publishConfig
section to specify the GitHub package registry as the registry. Replace {USER_NAME}
and {repo-name}
with your actual GitHub username and repository name. Here's an example:
"publishConfig": {
"registry": "https://npm.pkg.github.com/@{USER_NAME}"
},
"name": "@{user_name}/{repo-name}",
Modifying the permissions for the GITHUB_TOKEN
: Go to the GitHub repository, click on settings, then navigate to Actions
. In the General
section, ensure that both Read and Write permissions
are accepted, then click Save
.
In your project’s root directory, create a .github/workflows
folder if it doesn't already exist.
Inside that folder, create a file named publish-package.yml
with the following content. here add your USER_NAME
in the scope.
Here, GITHUB_TOKEN
is automatically provided by GitHub Actions and is used for authentication when publishing the package.
name: publish on githubon:
push:
branches:
- master
jobs:
publish-gpr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 18
registry-url: https://npm.pkg.github.com/
scope: "@{USER_NAME}"
- run: npm install
- run: npm run test
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
So, once you push to the master
branch, this workflow will run automatically, and the package will be published.
You can find your library in the Packages
section of your GitHub repository.
You can refer to my repository for assistance if you encounter any errors.
Publishing on NPM
First, create an account on npmjs.com. Then, navigate to your profile settings and click on Access Tokens
.
Now, click on Generate Token
and select Classic Token
.
Now, provide a name for the token and select the type as Automation
, and it will generate the token.
Next, go to your GitHub repository’s settings, and within the Secrets and variables
section, navigate to Actions
. Click on New repository secret
Provide a name for the secret (this name will be used to access the token in the workflow), and then click Add secret
.
We have to add a prepare
script to your package.json
to specify a command that runs when the package is prepared for publishing.
{
...
"scripts": {
"prepare": "npm run build"
},
...
}
In your project’s root directory, create a .github/workflows
folder if it doesn't already exist. Inside that folder, create a file named npm-publish.yml
with the following content.
name: publish npmon:
push:
branches:
- main
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- name: Install dependencies
run: npm install
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
So, once you push to the main
branch, this workflow will run automatically, and the package will be published.
Now, if you check the Packages
section of your account, you will find your package.
You can refer to my repository for assistance if you encounter any errors.
To test the library locally, create a directory named example
and then run the following command to initialize a React app in that directory:
npm create vite@latest . -- --template react-ts
npm install
Now, go to the root directory and build your npm package with the following command:
npm run build
To ensure you’re using the same version of React as your example app, run the following command from the root directory:
npm link "./example/node_modules/react"
Return to your example app’s directory and link your npm package to the example app using the package name you specified in its package.json
. Replace {package-name}
with your package's name.
npm link {package-name}
You can check whether the package is linked or not using the following command:
npm ls --location=global --depth=0 --link=true
Now, import the UI component from the library and test easily locally.
//App.tsx
import { Button } from "sld-ui-lib"; //replace name with you lib namefunction App() {
return <Button text="Button" />;
}
export default App;
After running the npm run dev
command, you can test the locally published package.
Create a directory named example
and then run the following command to initialize a React app:
npm create vite@latest . -- --template react-ts
Using Npm Registry
To install a package from the npm registry, you can use the following command:
npm install sld-ui-lib
This command will install the package named sld-ui-lib
from the npm registry.
Using Github Registry
To obtain a personal access token from GitHub for accessing packages from the GitHub Package Registry, follow these steps:
- Go to GitHub and sign in to your account.
- Click on your profile picture in the top right corner and select
Settings
. - In the left sidebar, click on
Developer settings
. - Under
Access tokens
, click onPersonal access tokens.
- Next, click on the
Generate new token
button. - Provide a name for your token in the
Token name
field. - Under
Select scopes,
make sure to select the necessary permissions required for your use case. To access packages from the GitHub Package Registry, you typically needread:packages
permissions. - After configuring the settings, click the
Generate token
button at the bottom of the page. - GitHub will generate a personal access token for you.
You can use this personal access token in your npm or yarn configuration when authenticating with the GitHub Package Registry.
To install a package from the GitHub package registry, create a .npmrc
file in your project and add the below configuration. Here, replace GITHUB_USER_NAME
with your GitHub username.
@GITHUB_USER_NAME:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
We shouldn't include the access token directly in this file, as it is tracked by Git. Instead, open a terminal and set an environment variable for your npm token. To do that, run the following command with your actual token:
export NPM_TOKEN=YOUR_TOKEN
You may want to add this export command to your shell profile (e.g., .bashrc, .zshrc) to make it persistent.
When you run npm commands, it will automatically substitute ${NPM_TOKEN}
with the actual token from the environment variable.
Replace your-username
with your GitHub username and package-name
with the library name and run it. It will install the package from the registry.
npm install @your-username/package-name
We are going to use Netlify for the deployment of our Storybook.
First, create an account on Netlify with GitHub and select the repository. Now, add the configuration as shown in the image below.
Here, we are running npm run build-storybook
to generate static files for Storybook, which will be placed in the storybook-static
directory.
Click ‘deploy,’ and Storybook is successfully deployed on Netlify.
This comprehensive guide covered essential tools and best practices for building a high-quality React component library — from project setup and testing to publishing, deployment, and local testing. Following these steps will enable you to create reusable, robust React components suitable for use across projects and teams. Whether building an open-source library or internal UI toolkit, you now have a solid foundation to develop shareable React components.
Feel free to adapt and extend these practices to suit the specific needs of your component library project. Happy coding!
For more updates on the latest tools and technologies, follow the Simform Engineering blog.