Steps to Create a React Component Library

Steps to Create a React Component Library

This is a step-by-step guide for creating a component library using TypeScript, Rollup, Jest for testing, styled-components for styling, Storybook for Component Library development, and CircleCI for automation.

The guide covers the following main sections:

  • Project Structure and Component Creation: Setting up the project structure and creating the first component.
  • Component Dependencies and Export: Installing necessary dependencies and exporting components.
  • Module Bundler Setup with Rollup: Installing and configuring Rollup for bundling.
  • Library Configuration and Testing: Updating package.json and configuring Jest for testing.
  • Unit Testing and Styling: Creating unit tests and integrating styled-components for styling.
  • Storybook Integration: Setting up Storybook for visualizing components.
  • Publishing to npm: Building the package and publishing it to npm.
  • Base Setup with Room for Best Practices: Providing additional recommendations for linting, versioning, and automation with CircleCI.

The provided CircleCI configuration file (config.yml) sets up a pipeline to automate tasks like dependency installation, linting, testing, building, releasing, and publishing the package to npm.

Overall, the guide offers a comprehensive approach to creating a component library and suggests additional best practices for improving code quality and workflow efficiency.

Initialization and TypeScript Setup

  • Run
npm init.
  • Fill in the details.
  • Install TypeScript as a dev dependency:
npm i --save-dev typescript.
  • Initialize TypeScript with: npx tsc --init.
  • Add TypeScript configurations to your tsconfig.json file.
{
  "compilerOptions": {
    /* Language and Environment */
    "target": "esnext",
    "jsx": "react-jsx",
    
    /* Modules */
    "module": "ESNext",
    "moduleResolution": "Node",
    
    /* JavaScript Support */
    "allowJs": false,
    "maxNodeModuleJsDepth": 1,
    
    /* Emit */
    "declaration": true,
    "emitDeclarationOnly": true,
    "sourceMap": true,
    "outDir": "dist",
    "declarationDir": "types",
    
    /* Interop Constraints */
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    
    /* Type Checking */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "allowUnreachableCode": false,
    
    /* Completeness */
    "skipLibCheck": true
  }
}

Project Structure and Component Creation

  • Create a .gitignore file and exclude node_modules.
  • Create the first component:
  • Create a src folder in the project root.
  • Inside src, create an index.ts file for exporting components.
  • Create a components folder inside src.
  • Within components, create the Page component.
// src/components/Page/index.tsx

import React from "react";
import { PageProps } from "./types";
import { Container } from "./styled";
const Page: React.FC<PageProps> = ({ title, children }) => {
  return (
    <Container>
      <h1>{title}</h1>
      {children}
    </Container>
  );
};
export default Page;
// src/components/Page/types.ts

export interface PageProps {
  title: string;
  children: React.ReactNode;
}

Component Dependencies and Export

  • Install necessary dependencies:
npm i --save-dev react react-dom @types/react.
  • Export components to src/index.ts file.
// src/index.ts

export { default as Page } from "./components/Page";

Module Bundler Setup with Rollup

  • Install Rollup:
npm i --save-dev rollup.
  • Configure the Rollup config file (rollup.config.mjs).
  • Add build scripts to package.json.
// rollup.config.mjs

import resolve from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
import commonjs from "@rollup/plugin-commonjs";
import dts from "rollup-plugin-dts";
import postcss from "rollup-plugin-postcss";
import packageJson from "./package.json";
export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
      },
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      resolve({
        extensions: [".js", ".jsx", ".ts", ".tsx"],
        skip: ["react", "react-dom"],
      }),
      commonjs(),
      typescript({
        tsconfig: "./tsconfig.json",
        exclude: ["**/*.test.tsx", "**/*.test.ts", "**/*.stories.ts"],
      }),
      postcss({ extensions: [".css"], inject: true, extract: false }),
    ],
    external: ["react", "react-dom", "react/jsx-runtime"],
  },
  {
    input: "dist/esm/types/index.d.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    plugins: [dts()],
    external: [/\.css$/],
  },
];

Library Configuration and Testing

  • Update package.json with main and module paths.
  • Install testing dependencies:
npm i --save-dev jest @testing-library/jest-dom @testing-library/react @types/jest jest-environment-jsdom
  • Configure Jest in package.json for testing.
"jest": {
  "testEnvironment": "jsdom"
}

Unit Testing and Styling

  • Create unit tests for components.
// src/components/Page/index.test.tsx

import "@testing-library/jest-dom";
import Page from ".";
import { render } from "@testing-library/react";
describe("Page", () => {
  it("renders title and children", () => {
    const title = "Test Title";
    const children = "Test Children";
    const { getByText } = render(<Page title={title}>{children}</Page>);
    const titleElement = getByText(title);
    const childrenElement = getByText(children);
    expect(titleElement).toBeInTheDocument();
    expect(childrenElement).toBeInTheDocument();
  });
  it("renders the correct styling", () => {
    const title = "Test Title";
    const children = "Test Children";
    const { getByTestId } = render(<Page title={title}>{children}</Page>);
    const container = getByTestId("page-container");
    expect(container).toHaveStyle(`
      background-color: #f5f5f5;
    `);
  });
});
  • Install styled-components:
npm i --save-dev styled-components.
  • Update components with styled-components for styling.
// src/components/Page/styled.ts

import styled from "styled-components";
export const Container = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 90vh;
  background-color: #f5f5f5;
`;
// src/components/Page/index.tsx

import React from "react";
import { PageProps } from "./types";
import { Container } from "./styled";
const Page: React.FC<PageProps> = ({ title, children }) => {
  return (
    <Container data-testid="page-container">
      <h1>{title}</h1>
      {children}
    </Container>
  );
};
export default Page;

Storybook Integration

  • Install Storybook:
npx storybook@latest init.
  • Delete stories that are pre-installed from Storybook.
  • Update .storybook/main.ts.
// .storybook/main.ts

import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;
  • Create Storybook stories for components.
// src/components/Page/Page.stories.tsx

import Page from ".";

export default {
  title: "MyComponents/Page",
  component: Page,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    title: {
      description: "The title of the Page",
      control: {
        type: "text",
      },
    },
    children: {
      description: "The children of the Page",
      control: {
        type: "text",
      },
    },
  },
};
export const PageOne = {
  args: {
    title: "This is the Page One title",
    children: "These are the Page One children",
  },
};
export const PageTwo = {
  args: {
    title: "This is the Page Two title",
    children: "These are the Page Two children",
  },
};

Publishing to npm

  • Build the package:
npm run build.
  • Create an account at npm.
  • Log in to your npm account in the terminal:
npm login.
  • Publish to npm:
npm publish.

Base Setup with Room for Best Practices

This guide provides a foundational setup for creating a component library. You can enhance this setup by implementing additional best practices such as linting, versioning, commit standards, and automation.

Linting and Code Standards

  • Integrate ESLint to enforce code style consistency.
  • Utilize Husky to set up pre-commit hooks for automatic linting.
  • Incorporate Prettier for code formatting consistency.

Versioning and Release Management

  • Implement Standard Version for automatic versioning based on commit messages.
  • Enforce conventional commits to ensure consistent and meaningful commit messages.

Automation with CircleCI

  • Set up a CircleCI pipeline to automate the build, test, release, and deployment processes.
  • Define workflows in a config.yml file to orchestrate CI/CD tasks efficiently.

Example config.yml file for CircleCI:

version: 2.1

orbs:
  node: circleci/node@5.0.2
jobs:
  lint_code:
    executor: node/default
    description: Lint code
    steps:
      - checkout
      - node/install-packages
      - run:
          name: Install dependencies
          command: npm install
      - run:
          name: Lint code
          command: npm run lint:fix
      - persist_to_workspace:
          root: .
          paths:
            - node_modules
            - package-lock.json
            - package.json
            - src
            - .eslintrc.js
            - .prettierrc.js
            - .gitignore
            - .npmignore
            - .nvmrc
            - .circleci
            - rollup.config.mjs
            - commitlint.config.js
            - tsconfig.json
            - .husky
  run_tests:
    executor: node/default
    description: Run tests
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Run tests
          command: npm run test
  build:
    executor: node/default
    description: Build
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Build
          command: npm run build
  run_release:
    executor: node/default
    description: Run release
    steps:
      - checkout
      - node/install-packages
      - run:
          name: Set up git credentials
          command: |
            git config --global user.email $GIT_EMAIL
            git config --global user.name $GIT_NAME
      - run:
          name: Run release
          command: |
            npm run release
            git push --follow-tags "@github.com/imran-codes/react-component-library.git">https://${GH_TOKEN}@github.com/imran-codes/react-component-library.git"
            main
  publish_to_npm:
    executor: node/default
    description: Publish to npm
    steps:
      - checkout
      - node/install-packages
      - run:
          name: Check if npm version is the same and publish to npm
          command: |
            CURRENT_VERSION=$(npm show imran-codes-react-component-library version)
            LOCAL_VERSION=$(node -pe "require('./package.json').version")
            if [ "$CURRENT_VERSION" != "$LOCAL_VERSION" ]; then
              echo "Versions are different. Publishing to npm."
              echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
              npm publish
            else
              echo "Versions are the same. Skipping npm publish."
            fi
workflows:
  build_app:
    jobs:
      - lint_code
      - run_tests:
          requires:
            - lint_code
      - build:
          requires:
            - run_tests
      - publish_to_npm:
          requires:
            - build

By integrating these additional practices and tools, you can ensure code quality, maintainability, and efficiency throughout the development and deployment lifecycle of your component library. Also to note the final app scripts in package.json should look like this:

{
  "name": "imran-codes-react-component-library",
  "version": "0.0.17",
  "description": "This is a react component library from the imran codes youtube channel.",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "test": "jest",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "prepare": "husky",
    "lint": "eslint ./src/ --ext .ts,.tsx",
    "lint:fix": "eslint ./src --ext .ts,.tsx --fix",
    "release": "standard-version"
  },
  "jest": {
    "testEnvironment": "jsdom"
  },
  "babel": {
    "sourceType": "unambiguous",
    "presets": [
      "@babel/preset-env",
      "@babel/preset-typescript",
      [
        "@babel/preset-react",
        {
          "runtime": "automatic"
        }
      ]
    ]
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/imran-codes/react-component-library.git"
  },
  "author": "Imran",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/imran-codes/react-component-library/issues"
  },
  "homepage": "https://github.com/imran-codes/react-component-library#readme",
  "devDependencies": {
    "@babel/preset-env": "^7.23.9",
    "@babel/preset-react": "^7.23.3",
    "@babel/preset-typescript": "^7.23.3",
    "@commitlint/cli": "^18.6.0",
    "@commitlint/config-conventional": "^18.6.0",
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "@rollup/plugin-typescript": "^11.1.6",
    "@storybook/addon-essentials": "^7.6.13",
    "@storybook/addon-interactions": "^7.6.13",
    "@storybook/addon-links": "^7.6.13",
    "@storybook/addon-onboarding": "^1.0.11",
    "@storybook/blocks": "^7.6.13",
    "@storybook/react": "^7.6.13",
    "@storybook/react-vite": "^7.6.13",
    "@storybook/test": "^7.6.13",
    "@testing-library/jest-dom": "^6.4.2",
    "@testing-library/react": "^14.2.1",
    "@types/jest": "^29.5.12",
    "@types/react": "^18.2.55",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "eslint": "^8.56.0",
    "eslint-config-standard-with-typescript": "^43.0.1",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-n": "^16.6.2",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-react": "^7.33.2",
    "husky": "^9.0.10",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rollup-plugin-dts": "^6.1.0",
    "rollup-plugin-postcss": "^4.0.2",
    "standard-version": "^9.5.0",
    "storybook": "^7.6.13",
    "styled-components": "^6.1.8",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  },
  "peerDependencies": {
    "react": ">=16"
  }
}

Conclusion

This guide offers a step-by-step approach to building a robust component library using modern tools and best practices. By following the outlined steps, developers can establish a solid foundation for creating reusable UI components with TypeScript, Rollup, Jest, styled-components, Storybook, and CircleCI. 

Through careful setup, structuring, configuration, and integration of testing and styling solutions, developers ensure code quality, maintainability, and scalability. Additionally, incorporating Storybook for component visualization and documentation, along with CircleCI for automation, streamlines development processes. 

By adopting recommended practices like linting, versioning, commit standards, and further automation, teams can enhance collaboration, code consistency, and release management.  Overall, this guide empowers developers to accelerate development cycles, improve code quality, and deliver exceptional user experiences.

See more blogs on my site also or check out videos on the Imran Codes Youtube Channel!