Creating a Typescript Component Library

Sun Feb 12 2023

Intro

Building component libraries is a great side project for any front-end developer who is looking to get more practice with react, typescript, deployment, or enterprise tools (chromatic, storybook, rollup). On top of the skills you learn, you also come out of the project with plug-and-play components that you can leverage in future projects.

This guide will take you step by step through the process of creating a typescript library, publishing it to NPM, and then deploying the storybook to Chromatic so you can share your component library with others.

Link to Published Storybook

Link to Example Repository

Step 1: Create a Git repository for your project

Before we even get into our text editor, make sure to create a remote repo for your library to live in.

  1. Log in to your Github account
  2. Click the green button that says “New”
  3. Name the repository

I named mine y2k-component-library

  1. Set the repository to Public
  2. Initialize the local directory where your Library lives with

git remote add origin https://github.com/username/REPO_NAME

Step 2: Configuring the workspace

Now that we have source control configured, it’s time to start defining the settings that dictate how our project will behave.

  1. Run npm init to initialize your directory as a package
  2. Create a .gitignore file and add node_modules
  3. Add dev dependencies:
npm i -D react react-dom typescript
  1. Create a tsconfig.json file to define typescript behavior in our project
{
  "compilerOptions": {
    "target": "ES2016", 
    "esModuleInterop": true, 
    "forceConsistentCasingInFileNames": true,
    "strict": true, 
    "skipLibCheck": true,
    "jsx": "react", 
    "module": "ESNext",
    "sourceMap": true,
    "outDir": "dist",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "types": [
      "aria-query"
    ]
  }
}

This is what my tsconfig looked like for my project. If you wish to customize any of the settings, feel free to tweak as needed.

  1. Define file structure for components
└── src/
    ├── components/
    │   ├── component-1/
    │   │   ├── component-1.css
    │   │   ├── component-1.tsx
    │   │   └── index.ts
    │   ├── component-2/
    │   │   ├── component-2.css
    │   │   ├── component-2.tsx
    │   │   └── index.ts
    │   └── index.ts
    └── index.ts

There are a lot of different ways to do this, so take my template as a suggestion and not a hard rule. We include numerous index.ts files within our project to allow users of our library to have an easier time importing the components they need.

Each component’s index.ts needs to include export {default} from ‘’./component-1

The index.ts in the components directory should include an export for each of the components you create. Mine looks like this:

export { default as Button } from "./Button";
export { default as Image } from "./Image";
export { default as List } from "./List";
export { default as Nav } from "./Nav";
export { default as Section } from "./Section";
export { default as Snippet } from "./Snippet";
export {default as NavItem} from './NavItem'

And FINALLY, the index.ts in the src directory needs just 1 line:

export * from './components'

  1. Now that we have our folders organized, we initialize Storybook with

npx storybook init 7. Once Storybook has initialized, it should have created a .storybook directory, open the main.js file in there and make sure it looks like this:

  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions"
  ],
  "framework": "@storybook/react"
}

By defining these paths, we can now add a .stories to each of our component folders and Storybook will automatically find them and compile them with the command npm run storybook

  1. Run npm run storybook from your terminal to see the example stories from initialization.

NOTE: you may run into an error that says “0308010C:digital envelope routines::unsupported”

To solve this, create a .npmrc file and paste this in there: legacy-peer-deps=true

Then, update the storybook script in package.json to look like this

"storybook": "export NODE_OPTIONS=--openssl-legacy-provider; start-storybook -p 6006",

Step 2: Creating your first Story

We now have the foundation of our project created, now we can move to creating stories for the components you created.

NOTE: This guide assumes you made components yourself between Step 2 and Step 3.

  1. Within one of your component directories, create a stories file based on the name of your component. I have a component called “Image” so I named the associated story file Image.stories.tsx
  2. Make the necessary imports
import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";

import Image from "./Image";
  1. Now we get to define how our component will behave in Storybook
export default {
  title: 'Image', //What to name Story
  component: Image, //Component we imported
  argTypes: {
    shape: {control: 'radio', options: ['DEFAULT', 'LEFT_CHEVRON', 'RIGHT_CHEVRON', 'PARALLELOGRAM']}
  },
} as ComponentMeta<typeof Image>;

The argTypes object is what we use to customize the fields in Storybook that change the props! It is a super powerful tool from Storybook with a ton of different configurations. Visit the documentation to see all the different options. Right now, I am using a radio button to switch between the options for my shape prop.

  1. Define template for your component
const Template: ComponentStory<typeof Image> = (args) => <Image {...args}/>
  1. Now you can define examples based on the template, and show the different options in the Storybook!
export const RightChevron = Template.bind({});
RightChevron.args = {
  src:'https://www.slashfilm.com/img/gallery/astro-boy-reboot-everything-we-know-so-far/l-intro-1657292510.jpg',
  alt: 'astroboy',
  shape: 'RIGHT_CHEVRON'
}

You can include any number of these exports in your Stories file and they will all appear under the Title we passed in at #3

Step 4: Getting ready for deployment

In order to make our components as light as possible for users, we are going to leverage rollup

  1. Install Rollup and necessary plugins
npm i -D rollup rollup-plugin-typescript2 rollup-plugin-peer-deps-external rollup-plugin-cleaner @rollup/plugin-commonjs @rollup/plugin-node-resolve
  1. Create a rollup.config.mjs file
  2. Fill the file with configuration preferences. Assuming you have the same setup as mine, it should look something like this:
import typescript from 'rollup-plugin-typescript2';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import cleaner from 'rollup-plugin-cleaner';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import packageJson from "./package.json" assert { type: "json" };
import css from "rollup-plugin-import-css";

export default {
  input: 'src/index.ts',
  output: [
    {
      file: packageJson.main,
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: packageJson.module,
      format: 'esm',
      sourcemap: true,
    },
  ],
  plugins: [
    cleaner({
      targets: ['./lib'],
    }),
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({
      exclude: ['**/*.stories.tsx', '**/*.test.tsx'],
    }),
    css(),
  ],
};
  1. Update package.json for deployment: we need to add main, module, and files fields, as well as a script to run the Rollup process.
  "main": "lib/index.js",
  "module": "lib/index.esm.js",
	"scripts": {
	...
	 "rollup": "rollup -c",
	}
	"files": [
    "lib"
	 ],		
  1. Now run rpm run rollup and it should create a file called lib filled with some javascript and CSS files. NPM knows to use these generated files for our library because we declared “lib” in our files in package.json

Step 5: Deployment

We’re at the final leg of our journey: sharing our hard work with the world. First we will walk through publishing our package to NPM. Then set up our Storybook to be hosted on Chromatic

  1. Sign in (or register) @ npmjs.com
  2. Sign in to NPM from your terminal within your project with npm login
  3. Run npm publish —access=public

This is all you need to publish your library! Now you should be able to see your package in your account on npmjs.com

Now lets get this project hosted on Chromatic

  1. Create static storybook build of your project with npm run storybook-build

There may be a few issues with this command, similar to when we first tried to use npm run storybook for the first time. Change the storybook-build command to:

 "build-storybook": "export NODE_OPTIONS=--openssl-legacy-provider; build-storybook",

In your package.json

  1. Install chromatic with npm i -D chromatic
  2. Log into Chromatic
  3. Select “Choose GitHub repo”
  4. Select the repo that houses your library
  5. Copy the unique project token
  6. Run npx chromatic —project-token=YOUR_PROJECT_TOKEN
  7. Once that has run it will generate a link for you in the terminal
  8. Click the link and follow the rest of the configuration steps
  9. You’re done!