DesignDev.io logoDesignDev.io logo

  1. Home
  2. /React Foundations
  3. /How to Build a Component Library with Proper Versioning
← Back to home

How to Build a Component Library with Proper Versioning

Alex Chen

Alex Chen

Featured
Featured
Cover Image for How to Build a Component Library with Proper Versioning
Alex Chen

Alex Chen

January 23, 2026·6 min read
Updated April 30, 2026
Series:React Foundations
Level:Intermediate
Tags:reactweb
Assumes:Comfortable with React, TypeScript, npm publishing, and monorepo concepts

Building a component library is easy. Building one that other developers can depend on — with proper exports, peer dependencies, versioning, and a changelog — is not. This is the complete setup that gets it right from day one.


This is the final article in the React Foundations series. Twenty articles from useEffect data fetching to component library distribution — the full journey from writing React to shipping it.

A component library is the natural endpoint of the patterns this series has covered. Compound components, render props, TypeScript generics, accessible modals, properly memoised lists — these patterns earn their full value when they're extracted into a shared library that multiple apps or teams can consume.

But most component libraries that developers build themselves fail in the same way: they work perfectly when imported from within the same project, and fall apart the moment another project tries to use them. Wrong peer dependencies. Missing type exports. No changelog. Opaque versioning.

This article is the setup that avoids all of that.


The three decisions before you write any code

1. Monorepo package or standalone repo?

Standalone repo: the library lives in its own repository. Independent release cadence. Published to npm. Consumed by other projects via npm install. Right when the library is genuinely shared across multiple codebases.

Monorepo package: the library lives alongside the apps that consume it. No npm publish step for internal use. Shared via workspace protocol. Right when the library is shared across apps in the same monorepo.

This article covers both — the internal setup is the same, the publishing step only applies to standalone repos.

2. What to use for bundling?

Three options worth knowing:

  • tsup — wraps esbuild, zero config for most cases, outputs CJS + ESM + types from one command. Best choice for most component libraries.
  • Rollup — more control, more config, better for complex transformation pipelines. Worth it when tsup doesn't cover your edge case.
  • Vite library mode — if you're already in a Vite ecosystem, vite build --mode lib is a natural choice.

This article uses tsup. It covers 95% of cases with 5% of the configuration.

3. CJS, ESM, or both?

Ship both. Every modern bundler supports ESM. Some toolchains still need CJS. The exports map in package.json lets consumers get the right format automatically.


The folder structure

The src/index.ts is the library's public API — the only file consumers should need to import from:

Everything exported here is public API. Everything not exported is an implementation detail — free to change without a version bump.


The package.json that actually works

Four fields that get this right:

exports map. The modern replacement for main. Bundlers that support exports use it to select the right format — import for ESM, require for CJS. Also enables subpath exports if you need them: "./button": { "import": "./dist/button.mjs" }.

files. Only the dist folder ships with the package. Source files, tests, config files — none of these should be in the published package. Use .npmignore as a fallback but files is cleaner and more explicit.

sideEffects: false. Tells bundlers this package has no side effects — every export can be tree-shaken. Set to ["./dist/index.css"] if you ship a CSS file.

peerDependencies. React must be a peer dependency, never a regular dependency. If React is in dependencies, npm might install a second copy of React alongside the consumer's React — causing the "invalid hook call" error that's notoriously difficult to debug.


The tsup.config.ts

Key options:

format: ['cjs', 'esm'] — outputs both formats in one build. tsup names them index.js (CJS) and index.mjs (ESM) automatically.

dts: true — generates .d.ts type declaration files. Without this, TypeScript consumers get any for everything.

external: ['react', 'react-dom'] — React is a peer dependency. It must not be bundled into the library output. If it were, consumers would get two React instances and hooks would break.

splitting: false — keeps the output as a single file per format rather than splitting into chunks. For component libraries, a single file is simpler to reason about. Enable splitting if the library is very large and you want tree-shaking at the file level.


The tsconfig.json

declarationMap: true — generates source maps for type declarations. When a consumer Cmd+clicks a component to see its definition, they see the actual TypeScript source rather than the generated .d.ts file.

jsx: "react-jsx" — uses React 17+'s automatic JSX transform. No import React from 'react' needed in every file.


Versioning: semantic versioning for UI components

Semantic versioning (major.minor.patch) means something specific:

  • Patch (0.1.0 → 0.1.1): bug fixes, no API changes. A consumer can update without changing anything.
  • Minor (0.1.0 → 0.2.0): new features, backward compatible. A consumer can update without changing anything but gets new capabilities.
  • Major (0.1.0 → 1.0.0): breaking changes. A consumer must make changes to update.

For component libraries, "breaking change" means:

The hardest call: changing a default value. If Button defaults to variant="primary" and you change the default to variant="secondary", every consumer who relies on the default gets a visual change they didn't ask for. That's a breaking change.


Changesets: automating version bumps and changelogs

Changesets is the tool that makes versioning disciplined without being painful. The workflow:

When you make a change:

An interactive prompt asks: patch, minor, or major? Describe the change. A markdown file is created in .changeset/.

When you're ready to release:

Changesets reads all the pending changeset files, bumps the version appropriately, and updates CHANGELOG.md. The changeset files are consumed and deleted.

To publish:

Publishes to npm and creates git tags for the release.

The CHANGELOG.md that Changesets generates is the artifact consumers actually use — the readable history of what changed and when.


Publishing to npm: the checklist before you run npm publish

The --dry-run step is the one most developers skip and shouldn't. It shows exactly what goes into the package before anything is uploaded. A published package with source files, test files, or config files included is unprofessional and bloated — and you can't unpublish once it's out.


Consuming the library

In a monorepo workspace:

From npm:

In both cases the import is identical:

TypeScript resolves types from the exports map. The consumer gets full autocomplete, type checking, and Cmd+click navigation to source.


What you give up without Vercel / Storybook / Chromatic

This setup publishes a functional, well-typed component library. What it doesn't include:

Visual documentation. Storybook gives you an interactive component explorer. Without it, consumers read the TypeScript types and the README. For a library used by one or two teams, that's often enough. For a public library, Storybook is worth the setup cost.

Visual regression testing. Chromatic (or Playwright screenshots) catches unintended visual changes. Without it, a CSS change that subtly breaks a component goes undetected until a consumer notices. Worth adding once the library has more than a handful of components.

Automated release workflow. A GitHub Actions workflow that runs Changesets, bumps versions, and publishes on merge to main removes human steps from the release process. Worth adding once the library has multiple contributors.

These are all additive — the foundation this article builds supports all of them without structural changes.


That's the React Foundations series. Twenty articles from the useEffect data fetching pattern you should replace to the component library setup you need to scale it.

The series covers React from the inside — the reconciliation algorithm, the concurrent rendering model, the hook primitives, the state management options — and from the outside — testing, accessibility, structure, distribution. The goal throughout has been the same: patterns you understand well enough to apply confidently and explain clearly.

If there's a concept from any article that deserves its own explainer, Muhammad Athar's "From the Builder" series is where it lives. Start with useEffect, What Is It Really? if you want to go deeper on the fundamentals.


The React Foundations series is complete. The next series — Next.js Advanced — starts with The App Router mental model: how Next.js thinks about rendering.

Related: pnpm workspaces — making monorepos actually manageable by Mira Halsted — the monorepo infrastructure that makes multi-package setups like this practical.

If you found this article interesting checkout our series on building your own react library from scratch.

Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.

Photo by Taylor Vick on Unsplash


More Stuff

Publishing Your First React Component to npm — the Complete Setup
April 1, 2026·3 min read

Publishing Your First React Component to npm — the Complete Setup

Most tutorials stop at "it works on my machine." This one doesn't. Publishing to npm is where your library becomes real — installable, versioned, and usable by anyone. Here's the complete setup, including the fields most developers get wrong in package.json and how to inspect what you're actually shipping before it goes live.

Muhammad Athar

Muhammad Athar

How to structure a UI library that scales — folders, exports, and naming conventions
April 1, 2026·3 min read

How to structure a UI library that scales — folders, exports, and naming conventions

The folder structure you pick on day one is the one you'll live with across 40 components. Get it wrong and you'll be fighting your own codebase by article 10. Get it right and adding a new component takes five minutes and follows a pattern every contributor can learn in one sitting.

Muhammad Athar

Muhammad Athar

DesignDev.io logoDesignDev.io logo

Explore

  • Home
  • Search
  • Authors
  • Series
  • About Us

Account

  • Sign in

Legal

  • Privacy Policy
  • Terms of Service

© 2026 DesignDev.io · All rights reserved

Built with Next.js · Tailwind · Sanity · Vercel