Building a React Component Library with Storybook and Rollup

TL;DR: Complete guide to building, documenting, and distributing a reusable React component library using TypeScript, Storybook, Rollup, and Jest

Building a shared React component library promotes consistency across applications and reduces duplication. This guide covers the complete workflow from development to distribution using modern tooling.

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                    COMPONENT LIBRARY                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│  │  TypeScript │  │   Rollup    │  │  Storybook  │             │
│  │   Source    │──▶│   Bundler   │  │    Docs     │             │
│  │   (.tsx)    │  │             │  │             │             │
│  └─────────────┘  └──────┬──────┘  └──────┬──────┘             │
│                          │                 │                     │
│                          ▼                 ▼                     │
│                   ┌─────────────┐  ┌─────────────┐             │
│                   │   .tgz      │  │  Static     │             │
│                   │   Package   │  │  Website    │             │
│                   └──────┬──────┘  └──────┬──────┘             │
│                          │                 │                     │
└──────────────────────────┼─────────────────┼────────────────────┘
                           │                 │
                           ▼                 ▼
                    ┌─────────────┐  ┌─────────────┐
                    │ S3 Bucket   │  │ CloudFront  │
                    │ (Package)   │  │ (Storybook) │
                    └─────────────┘  └─────────────┘

Technology Stack

ToolPurpose
TypeScriptType safety and better IDE support
RollupEfficient module bundling with tree-shaking
StorybookComponent documentation and visual testing
JestUnit testing framework
React Testing LibraryComponent behavior testing

Project Structure

library-root/
├── .storybook/
│   └── main.js
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.stories.tsx
│   │   │   ├── Button.test.tsx
│   │   │   └── index.ts
│   │   ├── Input/
│   │   │   ├── Input.tsx
│   │   │   ├── Input.stories.tsx
│   │   │   ├── Input.test.tsx
│   │   │   └── index.ts
│   │   └── index.ts          # Export all components
│   └── index.ts              # Main export file
├── build/                     # Generated after build
├── rollup.config.js
├── tsconfig.json
├── jest.config.js
├── babel.config.js
└── package.json

Setting Up the Project

Initial Setup

# Clone repository and install dependencies
git clone <repository-url>
cd component-library
npm install

# Start Storybook development server
npm run storybook

Storybook runs on port 6006 by default.

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext",
    "declaration": true,
    "declarationDir": "./build",
    "strict": true,
    "jsx": "react",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "build", "**/*.stories.tsx", "**/*.test.tsx"]
}

Rollup Configuration

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

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'build/index.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'build/index.esm.js',
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({ tsconfig: './tsconfig.json' })
  ]
};

Adding Components

Component File Structure

Each component follows a consistent four-file pattern:

// src/components/Button/Button.tsx
import React from 'react';

export interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  label,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  onClick
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
};

Storybook Stories

// src/components/Button/Button.stories.tsx
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger']
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large']
    }
  }
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  label: 'Primary Button',
  variant: 'primary'
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Secondary Button',
  variant: 'secondary'
};

export const Disabled = Template.bind({});
Disabled.args = {
  label: 'Disabled Button',
  disabled: true
};

Unit Tests

// src/components/Button/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with label', () => {
    render(<Button label="Click me" />);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('handles click events', () => {
    const handleClick = jest.fn();
    render(<Button label="Click me" onClick={handleClick} />);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('disables button when disabled prop is true', () => {
    render(<Button label="Disabled" disabled />);
    expect(screen.getByText('Disabled')).toBeDisabled();
  });
});

Export Component

// src/components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

// src/components/index.ts
export * from './Button';
export * from './Input';
export * from './Toast';
// ... export all components

// src/index.ts
export * from './components';

Building and Publishing

Build the Library

# Build the library bundle
npm run build-lib

# This creates:
# - build/index.js (CommonJS)
# - build/index.esm.js (ES Modules)
# - build/*.d.ts (TypeScript declarations)

# Create tarball package
npm run postbuild
# Creates: @company/components-library-x.y.z.tgz

Version Management

# Update version (interactive script)
./scripts/updateVersion.sh

# Options:
# - Patch: 1.0.0 → 1.0.1
# - Minor: 1.0.0 → 1.1.0
# - Major: 1.0.0 → 2.0.0

Local Testing

# Serve the package locally
serve -p 8085

# In consuming project, update package.json:
# "@company/components-library": "http://localhost:8085/components-library-1.2.3.tgz"

# Then install
npm install

Upload to S3

# Configure AWS CLI profile
aws configure --profile devs3
# Enter credentials when prompted

# Deploy package
./scripts/deploy.sh

# Deploy Storybook docs
npm run build-storybook
./scripts/deployStorybook.sh

Consuming the Library

Installation

# Install from CDN/S3
npm install https://cdn.example.com/library-build/components-library-1.2.3.tgz

Usage

// In your React application
import React from 'react';
import { Button, Input, Toast } from '@company/components-library';

function App() {
  return (
    <div>
      <Input placeholder="Enter text..." />
      <Button label="Submit" variant="primary" onClick={() => {}} />
      <Toast message="Success!" type="success" />
    </div>
  );
}

export default App;

Updating the Library

# Remove old version
rm -rf node_modules/@company/components-library

# Install new version
npm install @company/components-library

Storybook Features

Controls Panel

Interact with component props dynamically:

  • Modify values in real-time
  • Test edge cases
  • Discover component behavior

Actions Panel

Verify interactions produce correct outputs:

  • Track callback invocations
  • Inspect event payloads
  • Debug user interactions

Toolbar Features

FeaturePurpose
🔍 ZoomScale component for detail inspection
🖼 BackgroundTest on different backgrounds
📱 ViewportTest responsive designs

Auto-Generated Docs

The Docs tab provides:

  • Component API documentation
  • Prop types and defaults
  • Copy-paste code examples
  • Interactive demos

Best Practices

Component Guidelines

  1. Single Responsibility: Each component does one thing well
  2. Props Over State: Prefer controlled components
  3. TypeScript First: Define interfaces for all props
  4. Accessibility: Include ARIA attributes
  5. Testing: Achieve 80%+ coverage

Publishing Guidelines

  1. Always increment version before publishing
  2. Test package locally before S3 upload
  3. Update Storybook after component changes
  4. Document breaking changes in CHANGELOG
  5. Revert local package.json changes after testing

Performance Considerations

// Use React.memo for expensive components
export const ExpensiveComponent = React.memo(({ data }) => {
  // Render logic
});

// Use useCallback for event handlers passed as props
const handleClick = useCallback(() => {
  // Handler logic
}, [dependencies]);

This architecture enables teams to share UI components efficiently while maintaining documentation, type safety, and testability across multiple applications.