Introduction
Last year in September, I shared a series of blog posts, creating a react component library, using typescript
, styled-components
, styled-system
. I created a chakra-ui-clone
. I took a lot of inspiration from chakra-ui
on the component design and functionality and went through the code on github and in the process learned a lot. I used create-react-library
to scaffold my project. But if I were to create a new component library today, I would use other build tools like vite
or rollup
. I have updated my repository, it uses vite
as the build tool and latest versions of react
, react-dom
, typescript
and storybook with integration testing
. In this tutorial let us bootstrap a react component library using vite
as the build tool.
Why use another build tool
-
create-react-library
has not been updated for 2 years and is just a bootstrapping tool not a build tool. - When we create a new project using
create-react-library
it usescreate-react-app
version 3.4,react
version 16, the packages are all outdated. I remember updating all libraries for my library here. - Vite is a promising build tool and has been stable for almost a year now, it has a smaller footprint when it comes to installing dependencies and I saw a lot of improvements in the build time.
Links
- Using Vite in Library Mode here.
- Please read the best practices for packaging libraries guide here.
- Read more about publishing library here.
- You can check my tutorial series here.
- All the code for this tutorial can be found here.
- All the code for my
chakra-ui-clone
can be found here. - You can check the deployed
storybook
for mychakra-ui-clone
here.
Prerequisite
This tutorial is not recommended for a beginner, a good amount of familiarity with react
, styled-components
, styled-system
, and typescript
is expected. You can check my introductory post if you want to get familiar with the mentioned libraries. In this tutorial we will: -
- Initialize our project, create a GitHub repository.
- Install all the necessary dependencies.
- Add a
vite.config.ts
file. - Create our first component and build the project.
- Publish the project to private github package registery using npm.
- Setup Storybook.
- Setup eslint.
- Setup husky git hooks and commitizen.
Step One: Bootstrap Project.
First create a github repository. Then we will create a new folder and initialize git. From your terminal run -
mkdir react-vite-lib
cd react-vite-lib
git init
git remote add origin https://github.com/username/repo.git
Now we will run npm init and fill in the questionnaire, my package.json
is as follows -
{
"name": "@yaldram/react-vite-lib",
"version": "0.0.1",
"description": "A react library starter with styled components and vite as build tool.",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/yaldram/react-vite-lib.git"
},
"keywords": [
"react",
"typescript",
"vite"
],
"author": "Arsalan Yaldram",
"license": "ISC",
"bugs": {
"url": "https://github.com/yaldram/react-vite-lib/issues"
},
"homepage": "https://github.com/yaldram/react-vite-lib#readme"
}
Notice the name of the library; I have added my github username as prefix to the name this will be useful when we publish our library. Also notice the version is 0.0.1
as of now.
Let us now commit our code and push it to github -
git add .
git commit -m "feat: ran npm init, package.json setup."
git push origin master
- Now let use create a new branch called
dev
, the reason for that is simple. I like to have amaster
branch and adev
branch. - Assume we have many developers working on various features they will create pull requests from the dev branch. For that make sure you have changed the default branch from
master
todev
, check the github docs here. - We will merge all our feature and fix branches in the
dev
branch, test all our features on thedev deployment
. - When we want to ship these features to our users; we will raise a pull request with
master
as our base branch and finally merge all our code in the mainmaster
branch. This is what many might call arelease pull request
. - In this process we can also cherry pick commits, say you merged a feature into
dev
and want to test it further, we can skip this commit when we raise therelease pull request
. - This is how I used to do my backend projects on AWS ElasticBeanstalk and CodePipeline having 2 separate deployments.
- One our main public facing deployment triggered when we merge code to
master
branch. - Second our
dev
branch used by the Frontend and Q/A teams. We can test our new features here before shipping them. - Please feel free to share your workflow, or any imporvements that I should make, would love to hear your thoughts :).
git checkout -b dev
git push origin dev
Step Two: Installing all the necessary dependencies
From your terminal run the following -
yarn add -D react react-dom styled-components styled-system @types/react @types/react-dom @types/styled-components @types/styled-system
Now let us install vite and build related dependencies -
yarn add -D vite typescript @rollup/plugin-typescript tslib @types/node
-
vite
is our build tool. -
@rollup/plugin-typescript
is used for generating typescript.d.ts
files when we build the project. Also note we have installed all dependencies as dev dependencies. - Now we will add
react, react-dom, styled-components & styled-system
underpeerDependencies
in our package.json. - The consumer of our library will need to install these dependencies in order to user our library, when we build our project, we won't be bundling/including these 4 dependencies in our build. Here is my
package.json
file -
"devDependencies": {
"@rollup/plugin-typescript": "^8.5.0",
"@types/node": "^18.7.18",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.26",
"@types/styled-system": "^5.1.15",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.5",
"styled-system": "^5.1.5",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
"vite": "^3.1.2"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.5",
"styled-system": "^5.1.5"
}
In the root of our project create a new file .gitignore
add -
node_modules
build
dist
storybook-static
.rpt2_cache
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.npmrc
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Step Three: Setup vite
- Create a new folder in our project called
src
under it create a new fileindex.ts
. - Now from the root of our project create a file
tsconfig.json
-
{
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"target": "ESNext",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"jsx": "react",
"sourceMap": true,
"declaration": true,
"esModuleInterop": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx"]
}
In the root of the project create a file vite.config.ts
-
import { defineConfig } from 'vite';
import typescript from '@rollup/plugin-typescript';
import { resolve } from 'path';
import { peerDependencies } from './package.json';
export default defineConfig({
build: {
outDir: 'dist',
sourcemap: true,
lib: {
name: 'ReactViteLib',
entry: resolve(__dirname, 'src/index.ts'),
formats: ['es', 'cjs', 'umd'],
fileName: 'index',
},
rollupOptions: {
external: [...Object.keys(peerDependencies)],
plugins: [typescript({ tsconfig: './tsconfig.json' })],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDom',
'styled-components': 'styled'
}
}
},
},
});
})
- The output of the build will be in the
dist
folder. - Under the
rollupOptions
we first pass an array to theexternal
key which will not bundle our peer dependencies in our build. - We finally use the
@rollup/plugin-typescript
that will generate our typescript types. - In the
formats
array we specify our targets,cjs - common js
,es - esm
.umd
target is used when our library will also be used in thescript
tag. For theumd
format we have to mention theglobals
for theexternal dependencies
. I don't think we needumd
for our library here. But just wanted to highlight its usage .
In our package.json file add a scripts section -
"scripts": {
"build": "tsc && vite build"
},
Step Four: Add package entries in the package.json file
Now in our package.json file we need to add the following -
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"source": "src/index.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
In order to understand each and every entry above I would highly recommend you read this guide - https://github.com/frehner/modern-guide-to-packaging-js-library#packagejson-settings.
Step Five: Create the Box Component
I would recommend you read my tutorials here where I build multiple components, following the atomic design methodology. For this tutorial we will add a simple component to our library.
Under src
folder create a new file provider.tsx
-
import * as React from "react";
import { ThemeProvider } from "styled-components";
export const ViteLibProvider: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
return <ThemeProvider theme={{}}>{children}</ThemeProvider>;
};
Again under src
folder create a new file box.tsx
-
import styled from "styled-components";
import {
compose,
space,
layout,
typography,
color,
SpaceProps,
LayoutProps,
TypographyProps,
ColorProps,
} from "styled-system";
export type BoxProps = SpaceProps &
LayoutProps &
TypographyProps &
ColorProps &
React.ComponentPropsWithoutRef<"div"> & {
as?: React.ElementType;
};
export const Box = styled.div<BoxProps>`
box-sizing: border-box;
${compose(space, layout, typography, color)}
`;
Finally under src/index.ts
paste the following -
export * from './provider'
export * from './box'
From your terminal now run yarn build
and check the dist
folder. You should see 3 files namely index.js
(cjs), index.mjs
(esm) and index.umd.js
(umd).
Step Six: Publishing our Library.
- Previously while working with
create-react-library
, I used to test my packages locally. Now I publish my packages to a private GitHub registry using a.npmrc
and test them. - First, I would recommend you to please read this awesome tutorial on publishing here. We have already pushed our code to Github, next we need to add the
publishConfig
key to thepackage.json
.
"publishConfig": {
"registry": "https://npm.pkg.github.com/github-username"
}
- Next step we now have to create a
.npmrc
file. I had to create this file locally in my project and add it to.gitignore
due to some issue with my wsl setup. - On your github create a new Access token by navigating to
Settings -> Developer Settings -> Personal access tokens
, while creating the Access token make sure you check the following permission -
- Click on generate token and copy the token and save it somewhere. Now Paste the following to your
.npmrc
file -
registry=https://registry.npmjs.org/
@GITHUB_USERNAME:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=AUTH_TOKEN
To publish our package we run the following commands -
yarn install --frozen-lockfile
npm version minor
yarn build
npm publish
- First we install our dependencies.
- Then we run
npm version
to version our package. There are 3 options that we can givemajor | minor | prerelease
. When we givemajor
it bumps up our version say from0.1.0 -> 1.0.0
. If we giveminor
it bumps our version from0.1.0 -> 0.2.0
. - With the
prerelease
flag we can releasealpha & beta
versions of our package like so -
# for an alpha version like 0.0.2-alpha.0
npm version prerelease -preid alpha
# for an alpha version like 0.0.2-beta.0
npm version prerelease -preid beta
- In the next command we build our project.
Finally, we publish our project, using
npm publish
if we tag our project usingprerelease
flag we should usenpm publish --tag alpha
ornpm publish --tag beta
.Also, I would like to draw your attention to this library called changeset. It's very easyfor publishing and the best part is that you can use it in a GitHub action and automate the publishing tasks, release notes, version upgrades etc.
Special thanks to my colleague Albin for helping me understand the npm version command and its working, also suggesting the changeset
package. Please check the npm versioning docs here.
Step Seven: Test our published package
- Let us now create a new vite project and import our package, if you are using a local
.npmrc
make sure you copy it in this new project only then you can install our package -
npm create vite@latest react-demo-vite
cd react-demo-vite && npm install
npm install @yaldram/react-vite-lib
- In the
main.tsx
file paste the following -
import React from "react";
import ReactDOM from "react-dom/client";
import { ViteLibProvider, Box } from "@yaldram/react-vite-lib";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<ViteLibProvider>
<Box color="white" p="1rem" m="2rem" bg="green">
Hello my awesome package
</Box>
</ViteLibProvider>
</React.StrictMode>
);
From the terminal run
npm run dev
and test ourBox
component.Similarly, lets create another react project this time with create-react-app
npx create-react-app react-demo-cra --template typescript
cd react-demo-cra
yarn add @yaldram/react-vite-lib
- In the index.tsx file paste the following -
import React from "react";
import ReactDOM from "react-dom/client";
import { ViteLibProvider, Box } from "@yaldram/react-vite-lib";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<ViteLibProvider>
<Box color="white" p="1rem" m="2rem" bg="green">
Hello my awesome package
</Box>
</ViteLibProvider>
</React.StrictMode>
);
From the terminal run npm run start
and test our Box
component.
Step Eight: Add Flex component
Let us now add a new component to our package, and then we will publish it. Under src
create a new file flex.tsx
and paste -
import * as React from 'react';
import styled from 'styled-components';
import { system, FlexboxProps } from 'styled-system';
import { Box, BoxProps } from './box';
type FlexOmitted = 'display';
type FlexOptions = {
direction?: FlexboxProps['flexDirection'];
align?: FlexboxProps['alignItems'];
justify?: FlexboxProps['justifyContent'];
wrap?: FlexboxProps['flexWrap'];
};
type BaseFlexProps = FlexOptions & BoxProps;
const BaseFlex = styled(Box)<BaseFlexProps>`
display: flex;
${system({
direction: {
property: 'flexDirection',
},
align: {
property: 'alignItems',
},
justify: {
property: 'justifyContent',
},
wrap: {
property: 'flexWrap',
},
})}
`;
export type FlexProps = Omit<BaseFlexProps, FlexOmitted>;
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
(props, ref) => {
const { direction = 'row', children, ...delegated } = props;
return (
<BaseFlex ref={ref} direction={direction} {...delegated}>
{children}
</BaseFlex>
);
}
);
Flex.displayName = 'Flex';
Let us first commit these changes, after that lets publish a new version of our package, from your terminal -
yarn install --frozen-lockfile
npm version minor
yarn build
npm publish
Check whether the package is published, by visiting you github pages under the package
tab.
Now from the vite or cra demo apps, run -
npm install @yaldram/react-vite-lib@latest
Under the index.tsx/main.tsx
paste the following code -
import React from "react";
import ReactDOM from "react-dom/client";
import { ViteLibProvider, Box, Flex } from "@yaldram/react-vite-lib";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<ViteLibProvider>
<Flex h="80vh" color="white">
<Box size="100px" p="md" bg="red">
Box 1
</Box>
<Box size="100px" p="md" bg="green">
Box 2
</Box>
</Flex>
</ViteLibProvider>
</React.StrictMode>
);
Run npm run start / npm run dev
from the terminal to check if the Flex component works as expected.
Step Nine: Setup Storybook
From your terminal run -
npx storybook init
Storybook will automatically detect we are using vite and will use the vite bundler. Under the .storybook
folder now rename main.js
to main.tsx
and preview.js
to preview.tsx
and under preview.tsx
we will add our ThemeProvider as a decorator so that all our stories are wrapped by this Provider like so -
import React from "react";
import { Parameters } from "@storybook/react";
import { ViteLibProvider } from "../src/provider";
export const parameters: Parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
const withThemeProvider = (Story) => (
<ViteLibProvider>
<Story />
</ViteLibProvider>
);
/**
* This decorator is a global decorator will
* be applied to each and every story
*/
export const decorators = [withThemeProvider];
Under src
folder create a new story box.stories.tsx
and paste the following -
import * as React from "react";
import { StoryObj } from "@storybook/react";
import { Box, BoxProps } from "./box";
export default {
title: "Box",
};
export const Playground: StoryObj<BoxProps> = {
parameters: {
backgrounds: {
default: "grey",
},
},
args: {
bg: "green",
color: "white",
p: "2rem",
},
argTypes: {
bg: {
name: "bg",
type: { name: "string", required: false },
description: "Background Color CSS Prop for the component",
table: {
type: { summary: "string" },
defaultValue: { summary: "transparent" },
},
},
color: {
name: "color",
type: { name: "string", required: false },
description: "Color CSS Prop for the component",
table: {
type: { summary: "string" },
defaultValue: { summary: "black" },
},
},
p: {
name: "p",
type: { name: "string", required: false },
description: `Padding CSS prop for the Component shothand for padding.
We also have pt, pb, pl, pr.`,
table: {
type: { summary: "string" },
defaultValue: { summary: "-" },
},
},
},
render: (args) => <Box {...args}>Hello</Box>,
};
Now from the terminal run -
yarn storybook
Step Ten: Setup eslint
From your terminal run and complete the questionnaire -
yarn add -D eslint eslint-config-prettier eslint-plugin-prettier prettier
npx eslint --init
In your root create a file .prettierrc
-
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
Similarly create a file eslintrc.json
-
{
"env": {
"browser": true,
"node": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint", "prettier"],
"rules": {},
"settings": {
"react": {
"version": "detect"
}
}
}
Finally create 2 new files .eslintignore
& .prettierignore
-
build/
dist/
node_modules/
storybook-static
.snapshots/
*.min.js
*.css
*.svg
Under the scripts section of the package.json file paste the following scripts
"lint": "eslint . --color",
"lint:fix": "eslint . --fix",
"test:lint": "eslint .",
"pretty": "prettier . --write",
Step Eleven: Setup Husky hooks
From your terminal run the following -
yarn add -D husky nano-staged
npx husky install
nano-staged
is lighter and more performant than lint-staged
. Now from the terminal add a husky pre-commit
hook -
npx husky add .husky/pre-commit "npx nano-staged"
Add the following to the package.json -
"nano-staged": {
"*.{js,jsx,ts,tsx}": "prettier --write"
}
Step Twelve: Setup git-cz, commitizen (Optional)
I like to write git commits using git-cz and commitizen this step is optional you can skip it. Let us first install the necessary packages, run the following commands from your terminal -
yarn add -D git-cz commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact
In our package.json replace the default commitizen configuration with the one below -
"config": {
"commitizen": {
"path": "git-cz"
}
}
Add the following under the scripts section in package.json -
"commit": "git-cz"
Now under the root of our project add a file called changelog.config.js
and paste the following -
module.exports = {
disableEmoji: false,
list: [
'test',
'feat',
'fix',
'chore',
'docs',
'refactor',
'style',
'ci',
'perf',
],
maxMessageLength: 64,
minMessageLength: 3,
questions: [
'type',
'scope',
'subject',
'body',
'breaking',
'issues',
'lerna',
],
scopes: [],
types: {
chore: {
description: 'Build process or auxiliary tool changes',
emoji: '🤖',
value: 'chore',
},
ci: {
description: 'CI related changes',
emoji: '🎡',
value: 'ci',
},
docs: {
description: 'Documentation only changes',
emoji: '✏️',
value: 'docs',
},
feat: {
description: 'A new feature',
emoji: '🎸',
value: 'feat',
},
fix: {
description: 'A bug fix',
emoji: '🐛',
value: 'fix',
},
perf: {
description: 'A code change that improves performance',
emoji: '⚡️',
value: 'perf',
},
refactor: {
description: 'A code change that neither fixes a bug or adds a feature',
emoji: '💡',
value: 'refactor',
},
release: {
description: 'Create a release commit',
emoji: '🏹',
value: 'release',
},
style: {
description: 'Markup, white-space, formatting, missing semi-colons...',
emoji: '💄',
value: 'style',
},
test: {
description: 'Adding missing tests',
emoji: '💍',
value: 'test',
},
},
};
Let us now test our husky hooks and commitizen setup -
git add .
yarn commit
Now we will push our changes -
git push origin dev
git checkout master
git merge dev
git push origin master
Summary
There you go we have finally setup our library. All the code can be found here. In the next tutorial I would like to share my GitHub workflows for deploying my storybook / react spa to AWS S3.
Feel free to ask me any queries. Also, please leave your suggestion, let me know where I can improve and share your workflows. Your constructive feedback will be highly appreciated. Until next time PEACE.
Top comments (0)