The Case for Versioning Independent UI Components

Why you should really consider per-component semver instead of a single-version library.

Today, most components are versioned as part of an entire project (e.g. component-library ). This conflicts with the idea of composing modular UI applications. A developer that wants to adopt a slider component will have to introduce the whole library on a single version into their application. Every component on a page must comply with this library version.

Versioning single components, using SemVer, provides granular control over the composition of UI applications. It gives application builders the freedom to introduce shared components, continuously update partitioned parts of their UI without conflict, and retain their team independence.

Finally, we’ll demonstrate how tooling like Bit can help to independently version, distribute, update, and mix components to compose UI apps.

The problem with single-versioned libraries

A common practice is to single-version an entire component library.

library 1.0.0

├─ visual style

└─ components

├─ button

├─ button group

├─ card

├─ checkbox

├─ radio button

└─ ...

Most libraries are distributed as a single versioned package.

npm install @material-ui/core

This practice makes it hard to adopt components. It forces application builders to forfeit independence by coupling their development to that of the library. It forces them to align all component versions by the library. It forces them to update all components on every version bump regardless of relevance. It prevents them from delivering scoped updates to specific parts of their UI. It creates risks for conflicting dependency versions with the application.

There’s a better option: Versioning and distributing components separately by Semver rules. Today, SemVer is even adopted by design tools. Ideally, you can align the design and code of every component to the same version.

library

├─ visual style

└─ components

├─ button 5.3.1

├─ button group 2.1.0

├─ card 3.7.6

├─ checkbox 3.1.0

├─ radio button 1.1.0

└─ ...

By-component versioning provides a wide range of advantages, both for component builders and adopters. It lets component builders continuously communicate updates to specific components. It lets adopters get component-specific upgrades, without conflict. It helps shared components get adoption.

Advantages of by-component versioning

Here’s a short rundown on why independently versioning components is useful. I’d also suggest reading this wonderful post by Nathan Curtis.

Small, rapid incremental updates. Avoiding redundant dependency conflicts. Continuous releases, hotfixes, and rollbacks. Mix and match UI composition. Retaining team independence. Performance, stability, and dev velocity.

Let’s dive in.

Small, rapid incremental updates.

Versioning single components makes it possible to incrementally upgrade parts of a UI application. Semver rules provide powerful advantages for versioning components, by communicating changes as Major API breaking updates, backward compatible features and patch bug fixes.

When locking all shared components in a single version, adopters are forced to receive every update, whether it’s relevant to their code or not. Component distributors can’t introduce an update to a specific component, without coupling it to the versioning of all other components.

When versioning single components, adopters can get updates to components they use, to incrementally introduce scoped upgrades. It becomes possible to continuously get updates to specific components. SemVer rules greatly help in communicating different updates by major, minor and patch versions.

Tools like Bit facilitate this, both for component builders and adopters. It even provides features like auto-tagging dependent components upon changes.

Avoiding redundant dependency conflicts.

Developing internal components in as application and at the same time adding a third-party library, creates dependency versions conflicts.

For example, an application can have an internal “Scroller” component, which has “Scroll-JS” as a dependency on version 3.0.1 . A third-party library is added in order to introduce its “Button” component into the application. However, the library also has a “Scroller” component, which depends on “Scroll-JS” on version 2.0.2 . In this common case, conflicts can emerge.

Redundant conflict of dependency versions

Yet, if the application adopts only the “Button” component from the library, without the “Scroller” component, the conflict is avoided altogether.

Redundant conflict avoided!

Continuous releases, hotfixes, and rollbacks.

Single-versioned component libraries block the distributor’s ability to release incremental updates per-component. If there’s a new major version to “Slider”, it can’t be released until the next major version of the library. The same goes for minor and patch versions, which force a version update to the library.

On the other end, it forces adopters to introduce every library update into their own application. They’ll have to run CI every time there’s an update, whether it’s relevant to their application or not. At the same time, adopters can’t get -or suggest- minor or patch updates to specific components.

Versioning discrete components unlock the continuous release of upgrades and hotfixes, which can be incrementally introduced into adopting apps. When needed, a rollback or hotfix can be safely introduced too. Adopters can bump “Slider” from 1.0.0 to 1.0.1 without waiting for the next library release.

Mix and match UI composition.

SemVer’ing components is useful for adopting a set of shared components and combining them with ad-hoc versions, as needed.

For example, it’s possible to adopt a set of shared components versioned at 1.0.0 and ad-hoc update only “Slider” with a patch fix to version 1.0.1 — without conflict. In extreme cases, it’s even possible to introduce different versions of a component in the same page.

Using bit import , this can be done locally right in any adopting application. It’s even possible to introduce brand new components. Since all HTML markup, style and script is encapsulated within the component, it will not conflict with other types of components. Every component goes through a build-step in isolation, to ensure it works. This type of composition isn’t possible without per-component versioning and isolation.

Retain team independence.

When a team adopts a single-versioned library they couple the development of their application to that of the library. This means compromising control over its product roadmap and freedom to deliver upgrades. As a result, component libraries struggle to get adoption within organizations.

Versioning by component “legalizes” the adoption and modifications of components. By letting teams introduce single components in their applications, and giving them the freedom to mix and update with their own versions, it’s possible to adopt components and still retain independence.

Tools like Bit “legalize” the development of shared components. The bit import command lets adopting teams modify and update any shared component. They can mix versions of different components. They can add their own components and versions to the collective pool. It’s better to monitor and regulate than to block the adoption of components.

Performance, stability, and dev velocity.

Versioning single components enable greater development velocity and improve on the performance of the application built.

Performance — When an application only adds and uses the components it really needs, it reduces bundle-size. As a result, less code is parsed in the browser and execution time improves. Your users get a better experience, your SEO becomes better, and everybody is a bit happier. Tip- if using Bit.dev you can filter components by bundle-size and choose what to add to your app.

Stability — A single-versioned library forces adopting teams to constantly introduce wide changes to their application. SemVer’ed components increase stability by safely introducing incremental updates scoped to specific UIs. It‘s also easier to rely on unit-tests when upgrading single components. Bit independently tests and builds each version before introduced to the app.

Dev velocity — Team speed is increased when adopting shared components. When there’s an update needed, they can choose to ask for it, or update the component on their own (i.e. bit import ). Time is saved for both new feature development as well as ongoing codebase maintenance.

Per-component SemVer distribution using Bit

Bit is a tool that provides SemVer-based tagging of different components inside a single project (i.e. library or application). It lets you independently distribute each component, to be adopted and updated in UI applications.

Personally, I like to think of it this way: If a single-versioned component library is a CD Music Album, Bit is like iTunes or Spotify for components. It lets all teams share, adopt, update, and compose UI components together.

5 Min Demo: Version, distribute, update single UI components — using Bit

How it works

Using the bit add command Bit can track different components in a repository. It analyzes each and tracks all its files and dependencies as part of the component. It automates the packing process of the component, generating its package.json file and isolates its built/test environment. Later each component can be versioned, built, tested and exported on its own.

Using the bit tag command each component can be tagged with a SemVer. Upon tagging a version update, Bit prompts you which other components should be updated as well, and lets you auto-tag all of them together.

Versioned components can be exported and distriubted across teams and projects. When a component is updated with a new version, it can be independently updated by different applications, using SemVer rules.

Furthermore, Bit helps keep team independence. Every team can bit import shared components into their own project. There they can make changes, update a version, and export the update as a new version or new component. They can also quite easily add their own components to the collective tool.

Example: Tagging components in a project

This example shows a hello/world component. Let’s assume this component was already isolated and tacked by Bit (using bit addd ). It was already given independent build and test configurations (Bit automates this too). From this point, Bit treats the component as a standalone living unit inside the project.

If you run bit status you’ll see this output.

$ bit status

new components

> hello/world... ok

To tag the hello/world component, use bit tag.

$ bit tag hello/world

1 components tagged | 1 added, 0 changed, 0 auto-tagged

added components: hello/world@0.0.1

You can also tag all the components in the scope that are new or modified using the --all option.

$ bit status

new components

> hello/world... ok

> ui/button... ok

modified components

> string/pad-left... ok $ bit tag --all

3 components tagged | 2 added, 1 changed, 0 auto-tagged

added components: hello/world@0.0.1, ui/button@0.0.1

changed components: string/pad-left@1.0.2

It is also possible to tag all the components that are in the local scope, using the --scope option. If you specify a version number, Bit aligns all the components to the same version.

bit tag --scope 1.0.1 # all components on local scope are set to 1.0.1

By default, Bit creates a SemVer patch version for any tagged component. You can specify a version when tagging components, and Bit sets this version.

Bit can set a specific version when tagging a component.

$ bit tag hello/world 1.0.0

1 components tagged | 1 added, 0 changed, 0 auto-tagged

added components: hello/world@1.0.0

You can specify a SemVer increment, so Bit tags all the components using that increment. Bit supports patch , minor and major increments.

bit tag --all --major # Increment all modified and new components with a major version.

bit tag --all --minor # Increment all modified and new components with a minor version.

bit tag --scope --patch # Increment all components with a patch version.

bit tag --scope --patch # Increment all components in the workspace with a patch version.

Once tagged, components can be exported using bit export into a remote collection. From there they can be independently consumed.

Example: Updating and auto-tagging dependants

Bit manages dependencies between by storing the full dependency graph of the components. When Bit tags a component, it also tags any other Bit components that exist in the local scope and depends on it.

The dependent components are always tagged with a patch version, regardless of base component increment.

Let’s say we have 2 components: A navbar and a main-menu . The navbar is importing the mainmenu component as follow:

import MainMenu from '../main-menu/main-menu';

We track the components in bit, using the bit add command. bit status shows that components are added:

$bit status

new components

(use "bit tag --all [version]" to lock a version with all your changes) > main-menu ... ok

> navbar ... ok

We will tag both components so they are both on version 0.0.1 .

$bit tag --all

2 component(s) tagged

(use "bit export [collection]" to push these components to a remote")

(use "bit untag" to unstage versions) new components

(first version for components)

> main-menu@0.0.1

> navbar@0.0.1

Running bit status again now shows that the components are tagged:

$bit status

staged components

(use "bit export <remote_scope> to push these components to a remote scope") > main-menu. versions: 0.0.1 ... ok

> navbar. versions: 0.0.1 ... ok

Now, let’s make some changes in the code of main-menu , the dependency of navbar and run bit status again:

$bit status

modified components

(use "bit tag --all [version]" to lock a version with all your changes)

(use "bit diff" to compare changes) > main-menu ... ok

staged components

(use "bit export <remote_scope> to push these components to a remote scope") > main-menu. versions: 0.0.1 ... ok

> navbar. versions: 0.0.1 ... ok

components pending to be tagged automatically (when their dependencies are tagged)

> navbar ... ok

We see that main-menu is modified and as a result navbar is pending to be tagged as well, since its dependency was modified.

Now we will tag main-menu . As a result, we see that navbar is also tagged:

$bit tag main-menu

2 component(s) tagged

(use "bit export [collection]" to push these components to a remote")

(use "bit untag" to unstage versions) changed components

(components that got a version bump)

> main-menu@0.0.2

auto-tagged dependents: navbar@0.0.2