React UI Library with Storybook

Storybook is an open source tool for building UI components and pages in isolation. We can streamline our UI development, testing, and documentation using Storybook. This is particularly beneficial when building a shared component library that is used in multiple projects because we can develop entire UIs without needing to run a complex dev environment.

Storybook provides a sandbox to build UIs in isolation.

Let's explore Storybook 6 by creating the initial framework of an isolated React component library. We will be using Typescript and Rollup to bundle our project and get it ready to be publish on NPM.


Setting up the project

Create a folder for the project and cd into it.

mkdir tyger-ui && cd tyger-ui

Create a package.json file to set up a new npm package.

npm init -y

Install React, React Dom, and TypeScript.

npm i --save-dev react react-dom @types/react typescript

React requires a single copy of react-dom, let's add it as a peerDependency so that our package always uses the installed client's version. On package.json add the following snippet.

... "peerDependencies": { "react": "^16.8.0", "react-dom": "^16.8.0" }, ...

Lastly, let's create a tsconfig.json in the root directory to specify the root files and the compiler options required to compile the project.

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

Make sure to initialize a git repo and add a gitignore file.


Install Storybook

Let's use Storybook's CLI to install it in a single command. In the project's root directory run:

npx sb init

During the installation process, Storybook will install all the required dependencies, scripts, configuration files, and a few boilerplate stories to get us started.

Configuration file

The main configuration file is in: .storybook/main.js. This file controls the behavior of the Storybook server, we must restart Storybook’s process when we change it. Let's update the configuration structure tree to make Storybook find our stories inside a src folder.

module.exports = { stories: ['../src/**/*.stories.tsx'], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], };

Next, create the src folder in the root directory. This is where we will be creating our components and their stories. Let's restructure our project by creating a Button folder inside src/ and move the bolierplate Button's component files created by Storybook during the installation, located in the stories folder.

We'll be adding an entry file inside of each component.

/* src/Button/index.ts */ export { default } from './Button';

Let's cleaup generated component. Button.tsx, change export to export default. Also, export the interface ButtonProps. View the code on GitHub.

In addition, we need one entry file in src to export all the components.

/* src/index.ts */ import Button from './Button'; export { Button };

The file tree should look like this:

├── storybook │ ├── main.js │ └── preview.js ├── lib ├── node_modules ├── src │ ├── Button │ │ ├── button.css │ │ ├── Button.stories.tsx │ │ ├── Button.tsx │ │ └── index.tsx │ └── index.ts ├── .gitignore ├── package-lock.json ├── package.json └── tsconfig.json

Let's run the Storybook server to ensure that everything is working.

npm run storybook

Writting Stories

A story is a function that describes how to render the component. We'll write multiple stories per component to capture the UI variations and states that the component supports. The story file is for development only and it should be created alongside with the component's file.

Default export

The default export metadata controls how Storybook lists your stories and provides information used by addons. The ArgTypes describes the component's arguments and unlocks Storybook’s superpower of altering and composing arguments dynamically. Args can change props, slots, styles, inputs, etc. This specific feature can help us find edge cases in our components.

View the Component Library.

export default { title: 'Components/Button', component: Button, argTypes: { backgroundColor: { control: 'color' }, }, } as ComponentMeta<typeof Button>;

Defining stories

Example of how to render a Button in the "primary" state.

// Button.stories.tsx import React from 'react'; import { Meta } from '@storybook/react/types-6-0'; import { Story } from '@storybook/react'; import Button, { ButtonProps } from './Button'; export default { title: 'Components/Button', component: Button, argTypes: { backgroundColor: { control: 'color' }, }, } as Meta; export const Primary: React.VFC<{}> = () => <Button primary>Button</Button>;

In most cases, we'll have multiple stories per component. We can simplify the way we create stories by defining a master template for a component's stories, and use the default args and extend the object for customization. This will reduce code repetition and make our code more maintainable.

// Master template const Template: ComponentStory<typeof Button> = args => <Button {...args} />;
// Primary variation using the master template export const Primary = Template.bind({}); Primary.args = { primary: true, label: 'Button', }; // Secondary variation using the master template export const Secondary = Template.bind({}); Secondary.args = { label: 'Button', };

Compiling the UI Library with Rollup

Now, we'll be installing Rollup to compile our library so that we can start using it in our projects.

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

Next, create a rollup.config.js file in the root of our project.

// rollup.config.js 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 { // entry points input: './src/index.ts', // output files support both ESModules and CommonJS output: [ { file: packageJson.main, format: 'cjs', // commonJS sourcemap: true, }, { file: packageJson.module, format: 'esm', // ES Modules sourcemap: true, }, ], // Plugin array plugins: [ peerDepsExternal(), // prevents bundling peerDependencies resolve(), // resolves package entrypoints commonjs({ exclude: 'node_modules', ignoreGlobal: true, }), typescript({ useTsconfigDeclarationDir: true }), // typescript into js postcss({ extensions: ['.css'], // process CSS files }), ], };

Finally, add the Rollup build script package.json and the entries for our types and the ES Modules created by Rollup. This will help IntelliSense in code editors.

... "main": "lib/index.js", "module": "lib/index.esm.js", "types": "lib/index.d.ts", "scripts": { ... "clean": "rm -rf lib", // deletes lib on every build "build": "npm run clean && rollup -c" }, ...

Consuming our library

Now that we have our library bundled, we can Publish the component library on NPM. and start using it.

Import a component from our library:

import React from 'react'; import { Button } from 'tyger-ui'; // our library const App = () => <Button label="hello" size="small" />; export default App;

Link to the Github repo


Next steps

Be sure to checkout all available Storybook Addons and Rollup plugins to further customize the library according to your needs.