How to version React components
Managing breaking changes in shared React components is one of the most significant challenges in large-scale frontend architecture.
With over 25 years of experience in software development and as the creator of CoreUI, I have overseen the evolution of component libraries used by millions of developers, where versioning is the backbone of stability.
The most efficient and modern solution is to implement Semantic Versioning (SemVer) distributed via a package manager or managed through a monorepo with automated tools like Changesets.
This approach ensures that downstream applications can upgrade with confidence while maintaining a clear history of API modifications.
Use Semantic Versioning (SemVer) to categorize changes as Major (breaking), Minor (features), or Patch (fixes), and automate the process using tools like Changesets in a monorepo.
1. Implementing Semantic Versioning (SemVer)
The foundation of component versioning is SemVer. In a React context, this means your package.json version reflects the impact on the component’s public API (its props and behavior). A change to a prop name is a Major version bump, while adding a new optional prop is a Minor bump.
{
"name": "@my-org/ui-button",
"version": "2.1.4",
"description": "A versioned button component",
"peerDependencies": {
"react": ">=18.0.0"
}
}
This structure allows consumers to use tilde (~) or caret (^) requirements. For instance, ^2.1.4 allows any version that does not change the left-most non-zero digit. This is how we manage stability in CoreUI React components, ensuring that a patch update never breaks your layout.
2. Directory-Based Internal Versioning
When working within a single application where publishing to NPM is overkill, you can version components using directory structures. This allows you to run v1 and v2 of a component side-by-side during a migration phase, preventing “big bang” refactors that often lead to regressions.
// src/components/Button/v2/Button.tsx
import React from 'react'
export const Button = ({ children, variant = 'primary' }) => {
return (
<button className={`btn btn-${variant}`}>
{children}
</button>
)
}
// Consuming the specific version
import { Button } from './components/Button/v2/Button'
In this pattern, the v2 directory represents a significant architectural shift. Once the entire application has migrated, you can delete the v1 folder. This is a common strategy when upgrading complex systems like the CoreUI React Dashboard Template, allowing for incremental adoption of new design tokens.
3. Prop Deprecation Strategy
Instead of immediately breaking a component by removing a prop, use a deprecation strategy. This involves logging warnings in development environments to notify other developers that they need to migrate to a newer API before the next Major version release.
import React, { useEffect } from 'react'
export const Alert = ({ color, type, children }) => {
useEffect(() => {
if (color) {
console.warn('The "color" prop is deprecated. Use "type" instead.')
}
}, [color])
const alertType = type || color || 'info'
return (
<div className={`alert alert-${alertType}`}>
{children}
</div>
)
}
This ensures that developers see the warning in their console during development. Once all consumers have migrated to the new prop, you can safely remove the deprecated one in the next Major version release.
4. Automated Versioning with Changesets
In modern monorepos (like those using Turborepo or Nx), manually updating versions for dozens of components is error-prone. Changesets allow developers to document changes as they happen, and the tool handles the version bumping and changelog generation automatically.
# Adding a changeset
npx changeset
# The generated .changeset file looks like this:
# ---
# "@my-org/ui-card": minor
# ---
# Added "elevation" prop to support shadow levels.
When you are ready to release, running changeset version will look at all the markdown files in the .changeset directory, determine the correct SemVer bump for each package based on the accumulated changes, and update the package.json files. This is the gold standard for maintaining a professional React component library.
5. Versioning CSS and Design Tokens
React components are often tied to specific styles. Versioning the logic without versioning the styles can lead to “zombie” UI where the behavior is new but the look is old. Use CSS variables or scoped classes to version your visual layers alongside your React code.
/* button-v2.css */
.btn-v2 {
--btn-bg: #321fdb;
--btn-color: #fff;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
}
.btn-v2-large {
padding: 0.75rem 1.5rem;
}
By scoping your CSS to a versioned class like .btn-v2, you ensure that the styles for the new component do not leak into the old ones. This approach allows you to run both versions simultaneously during a migration period without visual conflicts.
6. Peer Dependency Management
When versioning React components, you must account for the version of React itself. If your component uses new features like Server Components or specific Hooks, you must strictly version your peerDependencies to prevent runtime errors in older consumer environments.
{
"name": "my-modern-component",
"version": "1.0.0",
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}
This prevents the component from being installed in a project that doesn’t meet the requirements. It acts as a “meta-version” that guards the integrity of your component’s environment, ensuring that the versioning of the component itself remains meaningful within its ecosystem.
Best Practice Note:
Always maintain a CHANGELOG.md file at the root of your component directory. Even with automated tools, a human-readable summary of what changed in each version is invaluable for other developers. At CoreUI, we treat the changelog as a primary piece of documentation, not an afterthought. For internal tools, consider using a tool like Bit to version individual components without the overhead of full NPM packages.



