Using Decorator + Builder Pattern for Managing Component Props
Proof of Concept (POC) Document
Title: Using Decorator + Builder Pattern for Managing Component Props in CoreDivider
1️⃣ Objective
The purpose of this POC is to explore an efficient, scalable, and maintainable way to define and manage props for React components using the Decorator and Builder design patterns.
The existing approach hardcodes prop definitions inside the component, making it less flexible. By using decorators and a builder pattern, we can make prop management modular, reusable, and easier to extend.
2️⃣ Problem Statement
Currently, the CoreDivider component defines valid and invalid props in a static array format. This has the following issues:
- ❌ Hardcoded Props: Any new prop must be manually added to the static list.
- ❌ Limited Reusability: No separation of concerns between prop definitions and component logic.
- ❌ Error-Prone: Risk of typos and inconsistencies when defining properties manually.
3️⃣ Solution Overview
The new approach follows a Decorator + Builder pattern:
- Decorators (
@PropMetadata) will be used to annotate class properties with metadata like type, default values, and valid options. - A Builder Pattern (
PropBuilder) will be used to construct the prop definitions dynamically. - A function (
extractProps()) will process the decorated class and generate prop definitions. - The CoreDivider component will use this extracted prop list instead of a hardcoded array.
4️⃣ Technical Implementation
Step 1: Define the Decorator for Metadata
function PropMetadata(options: { description?: string; type: string; default?: any; validValues?: any[] }) {
return function (target: any, key: string) {
if (!target.constructor.propDefinitions) {
target.constructor.propDefinitions = [];
}
target.constructor.propDefinitions.push({ name: key, ...options });
};
}
Step 2: Implement the Builder Pattern for Prop Definitions
class PropBuilder {
private name!: string;
private description?: string;
private type!: string;
private defaultValue?: any;
private validValues?: any[];
setName(name: string): this {
this.name = name;
return this;
}
setDescription(description: string): this {
this.description = description;
return this;
}
setType(type: string): this {
this.type = type;
return this;
}
setDefault(value: any): this {
this.defaultValue = value;
return this;
}
setValidValues(values: any[]): this {
this.validValues = values;
return this;
}
build() {
return {
name: this.name,
description: this.description,
type: this.type,
default: this.defaultValue,
validValues: this.validValues,
};
}
}
Step 3: Define CoreDivider Props Using Decorators
class CoreDividerProps {
@PropMetadata({ description: "Absolutely position the element.", type: "boolean", default: false, validValues: [true, false] })
absolute?: boolean;
@PropMetadata({ description: "The component used for the root node.", type: "elementType" })
component?: React.ElementType;
@PropMetadata({ description: "If true, ensures proper height in flex containers.", type: "boolean", default: false, validValues: [true, false] })
flexItem?: boolean;
@PropMetadata({ description: "If true, the divider will have a lighter color.", type: "boolean", default: false, validValues: [true, false] })
light?: boolean;
@PropMetadata({ description: "The component orientation.", type: "string", default: "horizontal", validValues: ["horizontal", "vertical"] })
orientation?: "horizontal" | "vertical";
@PropMetadata({ description: "The text alignment.", type: "string", default: "center", validValues: ["center", "left", "right"] })
textAlign?: "center" | "left" | "right";
@PropMetadata({ description: "The variant to use.", type: "string", default: "fullWidth", validValues: ["fullWidth", "inset", "middle"] })
variant?: "fullWidth" | "inset" | "middle";
@PropMetadata({ type: "boolean", validValues: [true, false] })
leftInset?: boolean;
@PropMetadata({ type: "boolean", validValues: [true, false] })
horizontalInset?: boolean;
@PropMetadata({ type: "boolean", validValues: [true, false] })
bold?: boolean;
@PropMetadata({ description: "The children of the component.", type: "React.ReactNode" })
children?: React.ReactNode;
}
Step 4: Extract Metadata and Build Props List
function extractProps(target: any) {
const props: any[] = [];
for (const prop of target.propDefinitions || []) {
const builder = new PropBuilder()
.setName(prop.name)
.setType(prop.type);
if (prop.description) builder.setDescription(prop.description);
if (prop.default !== undefined) builder.setDefault(prop.default);
if (prop.validValues) builder.setValidValues(prop.validValues);
props.push(builder.build());
}
return props;
}
export const validProps = extractProps(CoreDividerProps);
export const invalidProps = ["style", "theme"];
Step 5: Refactor CoreDivider Component
import React from "react";
import { NativeDivider } from "@wrappid/native";
import { sanitizeComponentProps } from "../../utils/componentUtil";
import { validProps, invalidProps } from "./CoreDividerProps";
interface CoreDividerProps {
absolute?: boolean;
component?: React.ElementType;
flexItem?: boolean;
light?: boolean;
orientation?: "horizontal" | "vertical";
textAlign?: "center" | "left" | "right";
variant?: "fullWidth" | "inset" | "middle";
leftInset?: boolean;
horizontalInset?: boolean;
bold?: boolean;
children?: React.ReactNode;
}
const CoreDivider: React.FC<CoreDividerProps> = (props) => {
const sanitizedProps = sanitizeComponentProps(CoreDivider, props);
const { children, ...restProps } = sanitizedProps;
return <NativeDivider {...restProps}>{children}</NativeDivider>;
};
// Assign extracted metadata
CoreDivider.validProps = validProps;
CoreDivider.invalidProps = invalidProps;
export default CoreDivider;
5️⃣ Expected Benefits
- ✅ Scalability: New props can be added just by defining a property in
CoreDividerProps. - ✅ Code Reusability: Metadata can be reused for validation, documentation, and prop-types generation.
- ✅ Separation of Concerns: Keeps metadata management separate from component logic.
- ✅ Easier Maintenance: No need to update multiple places when adding new props.
6️⃣ Testing & Validation Plan
- Unit Testing: Validate that
extractProps()correctly extracts and builds prop metadata. - Component Testing: Ensure
CoreDividerrenders correctly with both valid and invalid props. - Performance Benchmark: Compare the rendering time with the old approach.
- Code Review: Get feedback from the team for further improvements.
7️⃣ Conclusion & Next Steps
This POC demonstrates how the Decorator + Builder Pattern can improve prop management for CoreDivider. If validated, this pattern can be extended to other components in the project for consistency.
8️⃣ Future Enhancements
- 🔹 TypeScript Decorators for Auto-Validation: Use decorators for automatic runtime prop validation.
- 🔹 Schema Generation: Use decorators to auto-generate prop documentation and schema validation files.
- 🔹 Dynamic Prop Overrides: Allow external prop definitions through JSON or APIs.
💡 Final Thought
This approach ensures that prop definitions are structured, reusable, and scalable, reducing manual work and improving maintainability.