How to create a component library with React and StoryBook [REPO]

In the next post, we are going to do a step-by-step tutorial to implement a component library for React using Storybook and Rollup. I have deployed the project on GitHub Pages to share the final result with you:

https://juannunezblasco.github.io/react-rollup-storybook-library-starter/

We will cover everything from initial setup to publishing and distribution of the library. If you’re looking for a complete guide to creating and maintaining your own React component library, you’re in the right place.

You can see the complete project on GitHub:

https://github.com/juannunezblasco/react-rollup-storybook-library-starter

What is Storybooks

Storybook is a tool that allows developers to build components in isolation, making it easy to evaluate and test each component individually.

It also allows the creation of interactive, visual documentation for components, making it easier for other developers (and yourself later) to understand how each component works.

Additionally, Storybook offers a large number of plugins to expand its functionality, so it can be customized to meet the specific needs of your project.

The ultimate goal is to have a library of components that all members of your team use.

This will increase the productivity of your company, while improving the consistency of your application design.

Components to create in our library

We will create two simple components as a test that every application needs: a button and a form input.

// Button.tsx

import React from "react";
import "./Button.scss";

export interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>{
  label: string;
}

const Button = ({label, ...others}: ButtonProps) => {
  return <button {...others}>{label}</button>;
};

export default Button;
// Input.tsx

import React from "react";
import "./Input.scss";
import { Input, InputProps } from "antd";

export interface InputTextProps extends InputProps {
    /**
     * 
     */
}

const InputText:React.FC<InputTextProps> = (props: InputTextProps) => {
  return <Input type="text" {...props} />;
};

export default InputText;

Each of our components will have the same folder structure:

  • /tests : files with our tests
  • Button.css: styles we apply
  • Button.stories.tsx – StoryBook-specific implementation
  • Button.tsx – React native component
  • index.ts: barrel file where we export the component.

We are going to analyze the file with the StoryBook implementation because the rest is not much of a mystery.

// Button.stories.tsx

import { StoryFn, Meta } from "@storybook/react";
import React from "react";
import Button, { ButtonProps } from "./Button";

// More on default export: <https://storybook.js.org/docs/react/writing-stories/introduction#default-export>
const ExportDefault: Meta<typeof Button> = {
  title: "ReactComponentLibrary/Button",
  component: Button,
};

// More on component templates: <https://storybook.js.org/docs/react/writing-stories/introduction#using-args>
const Template: StoryFn<typeof Button> = (args) => <Button {...args} />;

export const HelloWorld: StoryFn<ButtonProps> = Template.bind({});
// More on args: <https://storybook.js.org/docs/react/writing-stories/args>
HelloWorld.args = {
  label: "Save",
};

export const ClickMe: StoryFn<ButtonProps> = Template.bind({});
ClickMe.args = {
  label: "Click me!",
};

export default ExportDefault;

The ExportDefault constant is where we include the title and component itself. We type it with the StoryBook interface including our component as a generic.

Then we create a template of our components by passing the properties to the component with the spread operator.

Based on this template we will build all the variations of our components by modifying their arguments. These will be the ones that we will see reflected in our component library.

Rollup.js Configuration

Rollup.js is a JavaScript module bundling tool that compiles small pieces of code into something larger and more complex, like a library or application.

It allows us to create JavaScript modules that can be imported and used in other projects.

Rollup.js is especially useful for use with libraries and frameworks like React due to its effectiveness in removing dead code and its efficiency in loading modules.

// rollup.config.js
import terser from '@rollup/plugin-terser';
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import postcss from "rollup-plugin-postcss";
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

import pkg from "./package.json" assert { type: 'json' };

export default [
	{
		input: 'src/index.ts',
		output: [
			{
				file: pkg.main,
				format: 'cjs',
				sourcemap: true,
			},
			{
				file: pkg.module,
				format: "esm",
				sourcemap: true,
			},
			/* {
				file: 'dist/bundle.min.js',
				format: 'iife',
				name: 'version',
				plugins: [terser()]
			} */
		],
		plugins: [
			peerDepsExternal(),
			resolve(),
			commonjs(),
			typescript({ tsconfig: "./tsconfig.json" }),
			postcss(),
			terser()
		]
	},
	{
		input: "dist/esm/types/index.d.ts",
		output: [{ file: "dist/index.d.ts", format: "esm" }],
		plugins: [dts()],
		external: [/\\.(css|less|scss)$/],
	},
];

For example, in this case we make a configuration so that the library supports the implementation using CommonJS and the modern Javascript standard ES6 and later.

The commented object would generate a version of the package that can be run directly in a browser without the need for a loaded module. Lo incluyo para que lo tengas en cuenta, pero para el caso que nos ocupa no nos interesa.

I include it so that you take it into account, but for the case at hand we are not interested.

First of all we have to configure the package.json.

{
  "name": "@juannunezblasco/react-rollup-storybook-library-starter",
  "version": "0.1.0",
  "description": "Template and quick-starter to create modern React libraries using Rollup.",
  "main": "dist/cjs/bundle.js",
  "module": "dist/esm/bundle.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "rimraf dist && rollup -c",
    "dev": "rollup -c -w",
    "test": "jest",
    "lint": "eslint src",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "predeploy-storybook": "npm run build-storybook",
    "deploy-storybook": "gh-pages -d storybook-static"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/juannunezblasco/react-rollup-storybook-library-starter.git"
  },
  "author": "juannunezblasco",
  "license": "MIT",
  "keywords": [
    "react-template",
    "react-library",
    "typescript",
    "react",
    "template"
  ],
  "bugs": {
    "url": "<https://github.com/juannunezblasco/react-rollup-storybook-library-starter/issues>"
  },
  "homepage": "<https://juannunezblasco.github.io/react-rollup-storybook-library-starter>",
  "peerDependencies": {
    "antd": "^5.4.0",
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "devDependencies": {
    ...
  }
}

We include all the information related to our package:

  • Name
  • Description
  • Repository
  • Routes
  • Keywords: relevant if it is a public package.
  • etc.

Like peerDependencies, which represent the dependencies that we expect to find in the application that will use our package.

Once our configuration is finished we can publish our package to npm:

npm publish

Conclusions

I recommend you follow the instructions that I have left in the README.dm to clone the repository and test the implementation.

If it has been helpful to you, share it on your social networks.

Juan Núñez Blaco

Full Stack Developer