DEV Community

Cover image for React component library setup with vite, typescript, styled-components, styled-system and storybook
Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

React component library setup with vite, typescript, styled-components, styled-system and storybook

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 uses create-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 my chakra-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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode
  • Now let use create a new branch called dev, the reason for that is simple. I like to have a master branch and a dev 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 to dev, check the github docs here.
  • We will merge all our feature and fix branches in the dev branch, test all our features on the dev 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 main master branch. This is what many might call a release 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 the release 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
Enter fullscreen mode Exit fullscreen mode

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

Now let us install vite and build related dependencies -

yarn add -D vite typescript @rollup/plugin-typescript tslib @types/node 
Enter fullscreen mode Exit fullscreen mode
  • 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 under peerDependencies 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"
  }
Enter fullscreen mode Exit fullscreen mode

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

Step Three: Setup vite

  • Create a new folder in our project called src under it create a new file index.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"]
}
Enter fullscreen mode Exit fullscreen mode

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'
        }
      }
    },
  },
});

})
Enter fullscreen mode Exit fullscreen mode
  • The output of the build will be in the dist folder.
  • Under the rollupOptions we first pass an array to the external 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 the script tag. For the umd format we have to mention the globals for the external dependencies. I don't think we need umd 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"
},
Enter fullscreen mode Exit fullscreen mode

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

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

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)}
`;
Enter fullscreen mode Exit fullscreen mode

Finally under src/index.ts paste the following -

export * from './provider'

export * from './box'
Enter fullscreen mode Exit fullscreen mode

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 the package.json.
 "publishConfig": {
    "registry": "https://npm.pkg.github.com/github-username"
  }
Enter fullscreen mode Exit fullscreen mode
  • 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 -

github-access-token

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

To publish our package we run the following commands -

yarn install --frozen-lockfile
npm version minor
yarn build
npm publish
Enter fullscreen mode Exit fullscreen mode
  • First we install our dependencies.
  • Then we run npm version to version our package. There are 3 options that we can give major | minor | prerelease. When we give major it bumps up our version say from 0.1.0 -> 1.0.0. If we give minor it bumps our version from 0.1.0 -> 0.2.0.
  • With the prerelease flag we can release alpha & 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
Enter fullscreen mode Exit fullscreen mode
  • In the next command we build our project.
  • Finally, we publish our project, using npm publish if we tag our project using prerelease flag we should use npm publish --tag alpha or npm 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
Enter fullscreen mode Exit fullscreen mode
  • 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>
);
Enter fullscreen mode Exit fullscreen mode
  • From the terminal run npm run dev and test our Box 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
Enter fullscreen mode Exit fullscreen mode
  • 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>
);
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

Now from the terminal run -

yarn storybook 
Enter fullscreen mode Exit fullscreen mode

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

eslint-questionnarie
In your root create a file .prettierrc -

{
  "trailingComma": "es5",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}
Enter fullscreen mode Exit fullscreen mode

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

Finally create 2 new files .eslintignore & .prettierignore -

build/
dist/
node_modules/
storybook-static
.snapshots/
*.min.js
*.css
*.svg
Enter fullscreen mode Exit fullscreen mode

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

Step Eleven: Setup Husky hooks

From your terminal run the following -

yarn add -D husky nano-staged
npx husky install
Enter fullscreen mode Exit fullscreen mode

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

Add the following to the package.json -

 "nano-staged": {
    "*.{js,jsx,ts,tsx}": "prettier --write"
  }
Enter fullscreen mode Exit fullscreen mode

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

In our package.json replace the default commitizen configuration with the one below -

"config": {
   "commitizen": {
      "path": "git-cz"
   }
 }
Enter fullscreen mode Exit fullscreen mode

Add the following under the scripts section in package.json -

 "commit": "git-cz"
Enter fullscreen mode Exit fullscreen mode

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

Let us now test our husky hooks and commitizen setup -

git add .
yarn commit
Enter fullscreen mode Exit fullscreen mode

Now we will push our changes -

git push origin dev
git checkout master
git merge dev
git push origin master
Enter fullscreen mode Exit fullscreen mode

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)