Photo by Kadarius Seegars on Unsplash

Easily build and publish a React / Typescript component library package to Npm using Storybook and Rollup.

If you have a collection of components that you would like to share and re-use in several projects, it’s very convenient to build your own library using Storybook and have it published on the npm package registry.

Marco Martino
Published in
7 min readJul 9, 2022

--

In this guide you’ll learn the steps required to have a React/Typescript component library ready and published!

Setup project with Storybook

Let’s init the project with create-react-app:

npx create-react-app my-library --template typescript

We can already install storybook:

# enter your project folder
cd my-library
# install storybook
npx storybook init

You can now run npm run storybook and that should boot up Storybook for you with the examples they created for you.

Once you are done playing with the example, you can go ahead and safely delete the stories folder content:

rm -rf src/stories/*

Finally, let’s create the components folder:

mkdir src/components

Setup dependencies

Since we are building a component library, all of the dependencies must live in dev. Open the package.json and move all dependencies in devDependencies, so that it looks like below:

{
"name": "my-library",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.9",
"@storybook/addon-essentials": "^6.5.9",
"@storybook/addon-interactions": "^6.5.9",
"@storybook/addon-links": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/node-logger": "^6.5.9",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.9",
"@storybook/testing-library": "^0.0.13",
"babel-plugin-named-exports-order": "^0.0.2",
"prop-types": "^15.8.1",
"webpack": "^5.73.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.43",
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.7.4",
"web-vitals": "^2.1.4"
}
}

Let’s update our packages:

npm install

Build Button component

We can now start to build our library components withButton. Create the file src/components/Button.tsx:

import * as React from "react";type ButtonStyle = {
[property: string]: string;
}
export interface ButtonProps {
label: string;
style: ButtonStyle;
onClick: () => void;
}
const Button: React.FunctionComponent<ButtonProps> = ({ label, style, onClick }) => {
return (
<button style={style} onClick={onClick}>{label}</button>
);
};
export default Button;

To finish, we can build our first story for Button. Create the file src/stories/Button.stories.tsx:

import { ComponentMeta, ComponentStory } from '@storybook/react';import Button from "../components/Button";export default {
title: "Example/Button",
component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;export const Default = Template.bind({});
Default.args = {
label: "Default button",
style: {
padding: "20px",
width: "20em",
margin: "10px",
color: "white",
fontSize: "19px",
cursor: "pointer",
border: "2px solid #fecd43",
background: "#fecd43",
},
onClick: () => {
console.log("You clicked the Default button");
},
};
export const White = Template.bind({});
White.args = {
label: "White button",
style: {
...Default.args.style,
color: "black",
background: "white",
border: "2px solid black",
},
onClick: () => {
console.log("You clicked the White button");
},
};
export const Small = Template.bind({});
Small.args = {
label: "Small button",
style: {
...Default.args.style,
padding: "3px",
width: "10em",
margin: "2px",
color: "white",
fontSize: "14px",
},
onClick: () => {
console.log("You clicked the Small button");
},
};

Build Map component

To build the Map component we need to install styled-components and react-leaflet. To install them:

npm install --save-dev leaflet @types/leaflet react-leaflet @types/react-leaflet

Create the file src/components/Map.tsx:

import * as React from "react";
import styled from "styled-components";
import { MapContainer, MapContainerProps } from "react-leaflet";
import "leaflet/dist/leaflet.css";interface MapWrapperProps {
width?: string;
height?: string;
minWidth?: string;
minHeight?: string;
}
const MapWrapper = styled(MapContainer)<MapWrapperProps>`
width: ${(props) => (props.width ? props.width : "100vw")};
height: ${(props) => (props.height ? props.height : "100vh")};
min-width: ${(props) => (props.minWidth ? props.minWidth : "400px")};
min-height: ${(props) => (props.minHeight ? props.minHeight : "400px")};
`;
interface MapElementProps {
width?: string;
height?: string;
minWidth?: string;
minHeight?: string;
}
const Map: React.FunctionComponent<MapElementProps & MapContainerProps> = ({
width,
height,
minWidth,
minHeight,
children,
...rest
}) => {
return (
<MapWrapper
width={width}
height={height}
minWidth={minWidth}
minHeight={minHeight}
{...rest}
>
{children}
</MapWrapper>
);
};
export default Map;

A story is needed for Map too. Create src/stories/Map.stories.tsx:

import { ComponentMeta, ComponentStory } from '@storybook/react';
import { TileLayer } from "react-leaflet";
import Map from "../components/Map";
export default {
title: "Example/Map",
component: Map,
} as ComponentMeta<typeof Map>;
const Template: ComponentStory<typeof Map> = (args) => <Map {...args} />;export const BasicMap = Template.bind({});
BasicMap.args = {
width: "80vw",
height: "80vh",
bounds: [
[42.5993718217880613, 1.5937492475355806],
[45.9312500000000341, 7.6656250000000341]
],
children: <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
};

You can now run again npm run storybook to boot up Storybook with the new components just created and play with it.

Setup peer dependencies

It’s time to add peerDependencies so that they can be satisfied by the client’s that they will consume our library. Let’s copy the following packaged from devDependencies to peerDependencies on package.json:

{  
...
"peerDependencies": {
"@types/leaflet": "^1.7.11",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-leaflet": "^2.8.2",
"@types/styled-components": "^5.1.25",
"leaflet": "^1.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.0.1",
"styled-components": "^5.3.5"
}
...
}

Setup library packaging with Rollup

At this point our library is ready to be packaged and published. To compile it, we’ll make use of Rollup.

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application. It uses the new standardized format for code modules included in the ES6 revision of JavaScript, instead of previous idiosyncratic solutions such as CommonJS and AMD. ES modules let you freely and seamlessly combine the most useful individual functions from your favorite libraries. This will eventually be possible natively everywhere, but Rollup lets you do it today.

First, make sure your tsconfig.json looks like this:

{
"compilerOptions": {
"outDir": "dist",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "dist"
},
"include": ["src"],
"exclude": [
"node_modules",
"src/stories"
]
}

Than we can install Rollup:

npm install --save-dev rollup rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-postcss postcss

To configure Rollup we need to create the rollup.config.js file:

import peerDepsExternal from "rollup-plugin-peer-deps-external";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "rollup-plugin-typescript2";
import postcss from "rollup-plugin-postcss";
const packageJson = require("./package.json");export default {
input: "src/index.tsx",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true
},
{
file: packageJson.module,
format: "esm",
sourcemap: true
}
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ useTsconfigDeclarationDir: true }),
postcss({
extensions: ['.css']
})
]
};

To be able to build our component library, let’s add rollup -c as build script on ourpackage.json:

{
...
"scripts": {
...
"build": "rollup -c",
},
...
}

Finally, let’s export all the components available on our library. Edit src/index.tsx to:

import Button from "./components/Button";
import Map from "./components/Map";
export { Button, Map };

We also need to define the files and the main entries for our library. So add the following to your package.json

{ 
...
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
...
}

Publish the package

We need to make some change to our package.json. Open it, and set the following values:

{
// The name of your new package (eventually the @organisation where you wish to publish it)
"name": "@soulweblab/my-library",
// Initial version you’re going to launch
"version": "0.1.0",
// Summary of what your package will do
"description": "This library is made by a Button and a Map components.",
// author: Your name, website, and (optionally) email
"author": "Soulweb Lab",
// private: We assume this is a public package
"private": false,
...
}

Make sure you have an npm account, than:

npm loginnpm publish --access public

Remember that, having a good README, LICENSE, and any additional information, will help to promote your library. We’ll skip this for now!

Consume the library

It’s time to consume and test the library just published! It’s just a matter of installing and importing it.

Let’s assume we are starting a project from scratch:

# create the my-app project
npx create-react-app my-app --template typescript
# enter my-app
cd my-app
# install our library
npm install --save @soulweblab/my-library

Edit App.tsx with the following code, to import and consume the library

import { Button, Map } from '@soulweblab/my-library';
import { TileLayer } from 'react-leaflet';
import type { LatLngBoundsExpression } from 'leaflet';
import "leaflet/dist/leaflet.css";const App = () => {
const args = {
label: "Default button",
style: {
padding: "20px",
width: "20em",
margin: "10px",
color: "lightblue",
fontSize: "19px",
cursor: "pointer",
border: "1px solid darkblue",
background: "darkblue",
},
onClick: () => {
console.log("You clicked the Default button");
},
};
const bounds: LatLngBoundsExpression = [
[42.5993718217880613, 1.5937492475355806],
[45.9312500000000341, 7.6656250000000341]
];
return (
<div>
<div>
<Button label="My Library" style={args.style} onClick={args.onClick} />
</div>
<div>
<Map width="100vw" bounds={bounds}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
</Map>
</div>
</div>
);
}
export default App;

The very last thing is to start and test it!

npm start

Hopefully, everything works as aspected and you’ll see a screen with a button and a map, just like this:

Conclusion

Having your Storybook components published makes it easy to maintain and re-use your code.

I hope you enjoyed this guide!

You can find the complete code at https://github.com/soulweblab/build-component-library-example.

The npm package is available at https://www.npmjs.com/package/@soulweblab/my-library.

--

--

Marco Martino
Soulweb Academy

Software Engineer, Open Source enthusiast, Full-stack Developer, Drupal specialist, React Developer, GIS Developer, Data lover