Introduction
In modern React development, maintaining consistency across your codebase is crucial for scalability, maintainability, and an improved developer experience (DX). A powerful approach to achieving this is creating a single source of truth for base components. This article explores the benefits of centralizing base HTML elements and guides you through building a foundational Html
component in TypeScript.
By the end, you will be able to use the new component as follows:
<Html.button
className={["btn", true && "btn-primary"]}
style={{ padding: "10px" }}
>
Click Me!
</Html.button>
<Html.a
href="https://example.com"
title="Example Site"
className={["link", { "link-active": isActive }]}
style={{ color: "blue" }}
>
Visit Example
</Html.a>
Why a Single Source of Truth Matters
Centralized Bug Fixes & Enhancements
- One Fix, Many Benefits: Updating the centralized logic for class names or props applies changes globally.
- Consistent Behavior: Derived components (e.g.,
<Html.button>
,<Html.a>
) behave uniformly, reducing inconsistencies.
Improved Developer Experience (DX)
- Extended Props: Enhancing native HTML elements (e.g., adding conditional class names) avoids repetitive code.
- Unified API: A consistent interface simplifies onboarding and usage.
Scalability & Maintainability
- Centralized Logic: Easier debugging, scaling, and updates.
- Reduced Boilerplate: Reusable logic minimizes code duplication.
Overview of Our Foundational Html
Component
Our goal is to create a centralized Html
object that:
- Inherits all native properties.
- Supports built-in class name merging.
- Is written in TypeScript with ref forwarding for type safety.
This approach promotes DRY (Don’t Repeat Yourself) principles, ensuring consistency and future-proofing enhancements.
Tutorial: Building the Component
Step 1: Setting Up Your Project
Initialize a new React project with TypeScript:
npx create-react-app my-app --template typescript
cd my-app
npm start
Step 2: Creating the Component Generator
Create factory.tsx
:
import React from "react";
function classNames(...args: any[]): string {
const classes: string[] = [];
args.forEach((arg) => {
if (!arg) return;
if (typeof arg === "string" || typeof arg === "number") {
classes.push(String(arg));
} else if (Array.isArray(arg)) {
const inner = classNames(...arg);
if (inner) classes.push(inner);
} else if (typeof arg === "object") {
for (const key in arg) {
if (arg[key]) classes.push(key);
}
}
});
return classes.join(" ");
}
export function factory<Tag extends keyof JSX.IntrinsicElements>(tag: Tag) {
const Component = React.forwardRef<HTMLElement, JSX.IntrinsicElements[Tag]>(
(props, ref) => {
const { className, ...rest } = props;
return React.createElement(tag, {
...rest,
ref,
className: classNames(
...(Array.isArray(className) ? className : [className])
),
});
}
);
Component.displayName = `Html.${tag}`;
return Component;
}
Step 3: Building the Html
Collection
Create html.tsx
:
import React from "react";
import { factory } from "./factory";
const tags = ["a", "button", "div", "span", "p", "h1", "h2"] as const;
type HtmlComponents = {
[K in (typeof tags)[number]]: ReturnType<typeof createHtmlComponent>;
};
const Html = {} as HtmlComponents;
tags.forEach((tag) => {
Html[tag] = factory(tag);
});
export default Html;
Step 4: Using the Html
Component
Modify App.tsx
to use the new components:
import React, { useState } from "react";
import Html from "./components/html";
import "./styles.css";
export default function App() {
const [isPrimary, setIsPrimary] = useState(true);
const [isActive, setIsActive] = useState(false);
return (
<div style={{ padding: "20px" }}>
<Html.button
className={["btn", isPrimary && "btn-primary"]}
onClick={() => setIsPrimary((prev) => !prev)}
>
Toggle Primary Button
</Html.button>
<Html.a
href="https://overctrl.com"
title="Example Site"
className="link"
style={{ display: "block", marginTop: "20px", color: "blue" }}
onClick={() => setIsActive((prev) => !prev)}
>
Visit overctrl
</Html.a>
</div>
);
}
export default App;
Demo
The Benefits of the HTML Component Approach
Adopting a single source of truth for your base HTML components unlocks several key advantages, making your codebase more consistent, maintainable, and scalable. Let's explore the primary benefits of this approach.
1. Consistent Behavior Across Your Application
By standardizing components like <Html.button>
or <Html.a>
, you ensure predictable behavior throughout your codebase. Any enhancements or bug fixes applied at the base level propagate seamlessly, eliminating inconsistencies in how class names are merged or props are managed.
2. Simplified Bug Fixes and Feature Enhancements
With a centralized component structure, debugging and feature updates become significantly more efficient. For instance, if an issue arises with how conditional class names are processed, updating the mergeClassNames
function applies the fix universally—saving time and effort.
3. Enhanced Developer Experience
A well-defined API reduces cognitive load for developers, ensuring they interact with a familiar, intuitive component structure. This consistency minimizes errors and accelerates development, while extensions such as theming and accessibility can be introduced effortlessly at a single touchpoint.
4. Improved Maintainability as Your Project Scales
As your application grows, managing a scattered and redundant component logic can become cumbersome. By centralizing logic within the Html
component, you maintain a cleaner and more structured codebase, reducing duplication and streamlining future updates.
Expanding Your HTML Component
Once you have established a foundational Html
component, there are numerous ways to extend its capabilities. Here are some powerful enhancements:
1. Theming and Styling Enhancements
- Global Theme Support: Integrate with styling libraries like
styled-components
orEmotion
to apply global themes effortlessly. - Responsive Design: Introduce responsive properties that allow developers to define styles dynamically based on breakpoints.
2. Accessibility Improvements
- Built-in ARIA Support: Automatically inject relevant ARIA attributes, ensuring accessibility compliance.
- Focus Management: Implement better keyboard navigation and focus state handling for interactive elements like buttons and links.
3. Animation and Transitions
- CSS Animations: Leverage animation libraries such as
Framer Motion
to provide seamless state transitions. - State Transitions: Introduce hooks for managing UI state transitions with minimal boilerplate.
4. Advanced Prop Validation and Warnings
- Runtime Checks: Issue warnings for deprecated or conflicting props during development.
- TypeScript Enhancements: Utilize stricter TypeScript interfaces for better compile-time validation.
5. Integration with Design Systems
- Design Tokens: Map design tokens (colors, spacing, typography) to CSS variables for consistency.
- Component Variants: Use variant props (
primary
,secondary
,danger
) to streamline styling and avoid redundant code.
6. Extending Beyond Standard HTML Elements
- Custom Elements: Apply the same structured approach to non-HTML elements like SVG icons or complex UI components.
- Higher-Order Components (HOCs): Wrap
Html
components with additional logic for logging, error boundaries, or performance monitoring.
Each of these enhancements further solidifies the value of using a single source of truth, ensuring your component system remains flexible and future-proof.
Conclusion
Standardizing base components within a React application is a powerful strategy for improving maintainability, scalability, and consistency. By implementing a foundational Html
component, you achieve:
- A centralized structure for className management and prop handling.
- A more intuitive developer experience with predictable component behavior.
- Simplified bug fixes and feature upgrades with minimal refactoring.
This approach reduces redundancy and sets the stage for future enhancements—whether through global theming, accessibility improvements, or animation integration.
We encourage you to experiment with this methodology in your projects and explore the extension ideas outlined above. Adopting a single source of truth will lead to a more efficient, maintainable, and enjoyable development experience.
Happy coding!
Discussion