Composite Pattern for UI Component Trees in React
Use the Composite pattern to compose objects into tree structures, letting clients treat individual objects and compositions uniformly in UI component hierarchies
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Composite Pattern for UI Component Trees in React
The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. In React, this pattern appears naturally when rendering nested component trees where a container holds both leaf elements and other containers. See the general Composite Pattern for language-agnostic examples.
When to Use This
- You have a tree structure of objects with parent-child relationships
- Clients should ignore the difference between compositions and individual objects
- You need to perform operations recursively across a hierarchy
Problem
A form builder needs to render nested groups, fields, and sections. The rendering logic branches for every type instead of treating everything as a renderable node.
Solution
// components/Composite.tsx
interface ComponentNode {
id: string;
type: string;
render(): React.ReactNode;
}
// Leaf
class FieldNode implements ComponentNode {
constructor(
public id: string,
public label: string,
public value: string
) {}
render(): React.ReactNode {
return (
<div key={this.id} className="field">
<label>{this.label}</label>
<input type="text" defaultValue={this.value} />
</div>
);
}
}
// Composite
class GroupNode implements ComponentNode {
public children: ComponentNode[] = [];
constructor(
public id: string,
public title: string
) {}
add(child: ComponentNode): void {
this.children.push(child);
}
remove(childId: string): void {
this.children = this.children.filter(c => c.id !== childId);
}
render(): React.ReactNode {
return (
<fieldset key={this.id} className="group">
<legend>{this.title}</legend>
{this.children.map(child => child.render())}
</fieldset>
);
}
getTotalFields(): number {
return this.children.reduce((count, child) => {
if (child instanceof GroupNode) {
return count + child.getTotalFields();
}
return count + 1;
}, 0);
}
}
// Usage
const formRoot = new GroupNode('root', 'User Profile');
const personalInfo = new GroupNode('personal', 'Personal Information');
personalInfo.add(new FieldNode('firstName', 'First Name', 'John'));
personalInfo.add(new FieldNode('lastName', 'Last Name', 'Doe'));
const address = new GroupNode('address', 'Address');
address.add(new FieldNode('street', 'Street', '123 Main St'));
address.add(new FieldNode('city', 'City', 'Springfield'));
formRoot.add(personalInfo);
formRoot.add(address);
// In a React component
function FormBuilder({ root }: { root: GroupNode }) {
return (
<form>
{root.render()}
<p>Total fields: {root.getTotalFields()}</p>
</form>
);
}
How It Works
- Component defines the common interface for all objects in the tree
- Leaf represents individual objects with no children
- Composite stores child components and implements child-related operations
- Client works with any Component uniformly via the common interface
Real-World Example: File System
// File system nodes
interface FileSystemNode {
name: string;
getSize(): number;
print(indent?: string): void;
}
class File implements FileSystemNode {
constructor(
public name: string,
private size: number
) {}
getSize(): number {
return this.size;
}
print(indent = ''): void {
console.log(`${indent}📄 ${this.name} (${this.size} bytes)`);
}
}
class Directory implements FileSystemNode {
public children: FileSystemNode[] = [];
constructor(public name: string) {}
add(node: FileSystemNode): void {
this.children.push(node);
}
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent = ''): void {
console.log(`${indent}📁 ${this.name}/`);
this.children.forEach(child => child.print(indent + ' '));
}
}
const root = new Directory('src');
const components = new Directory('components');
components.add(new File('Button.tsx', 1200));
components.add(new File('Card.tsx', 800));
root.add(components);
root.add(new File('index.ts', 150));
root.print();
console.log(`Total size: ${root.getSize()} bytes`);
Production Considerations
- Use TypeScript discriminated unions instead of classes for simpler React props
- Consider immutable tree updates with structural sharing for large hierarchies
- Add
parentreferences for upward traversal, but avoid circular JSON serialization
Common Mistakes
- Putting child management methods in the base Component interface, forcing Leaf to implement them
- Not handling deeply nested recursion that could exceed stack limits
- Mutating the tree structure during iteration
FAQ
Q: How is this different from Decorator? A: Composite builds tree structures with container semantics. Decorator adds responsibilities to a single object without tree semantics.
Q: When should I avoid Composite? A: When the hierarchy is flat (only one level) or when child operations make no sense for leaf nodes. For flat structures, consider Decorator instead.
Is this pattern suitable for small projects?
For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.
How does this pattern compare to alternatives?
Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.
Advanced Topics
Scenario: Composite for UI Component Tree
// Composite pattern: treat individuals and composites uniformly
interface UIComponent {
render(): string;
getChildren(): UIComponent[];
add(child: UIComponent): void;
remove(child: UIComponent): void;
}
// Leaf: component without children
class Button implements UIComponent {
constructor(private label: string, private style: string = "") {}
render(): string {
return `<button class="${this.style}">${this.label}</button>`;
}
getChildren(): UIComponent[] { return []; }
add(child: UIComponent): void { throw new Error("Cannot add to leaf"); }
remove(child: UIComponent): void { throw new Error("Cannot remove from leaf"); }
}
class Input implements UIComponent {
constructor(private type: string, private placeholder: string) {}
render(): string {
return `<input type="${this.type}" placeholder="${this.placeholder}" />`;
}
getChildren(): UIComponent[] { return []; }
add(): void { throw new Error("Cannot add to leaf"); }
remove(): void { throw new Error("Cannot remove from leaf"); }
}
// Composite: component with children
class Container implements UIComponent {
private children: UIComponent[] = [];
constructor(private tag: string = "div", private className: string = "") {}
add(child: UIComponent): void { this.children.push(child); }
remove(child: UIComponent): void {
this.children = this.children.filter(c => c !== child);
}
getChildren(): UIComponent[] { return [...this.children]; }
render(): string {
const childrenHTML = this.children.map(c => c.render()).join("");
return `<${this.tag} class="${this.className}">${childrenHTML}</${this.tag}>`;
}
}
// Usage: build UI tree
const form = new Container("form", "login-form");
const header = new Container("div", "form-header");
header.add(new Button("Close", "btn-close"));
const body = new Container("div", "form-body");
body.add(new Input("email", "Email"));
body.add(new Input("password", "Password"));
const footer = new Container("div", "form-footer");
footer.add(new Button("Submit", "btn-primary"));
form.add(header);
form.add(body);
form.add(footer);
console.log(form.render());
// <form class="login-form"><div class="form-header"><button class="btn-close">Close</button></div>...
Lessons:
- Composite treats leaves and composites with the same interface
- The client does not distinguish between a button and a container
- UI trees: each node is a UIComponent
- add/remove on leaves throws error: cannot have children
- React uses this pattern: elements are uniformly composable
### Composite vs Decorator: which do I use?
Composite is structural: trees of objects with the same interface. Decorator is structural: wraps one object to add behavior. Composite has 0..N children; Decorator has exactly 1. Composite builds trees; Decorator builds chains. Use Composite for UI hierarchies. Use Decorator to add logging, cache, or validation to a service. Related Resources
Decorator Pattern for HTTP Request Pipelines
Use the Decorator pattern to compose cross-cutting concerns like logging, metrics, and retries into HTTP request pipelines without modifying core logic
PatternAdapter Pattern for Integrating External REST APIs
Use the Adapter pattern to normalize responses from external REST APIs into a consistent internal model without leaking third-party formats into your domain
PatternMediator Pattern for Loose Component Coupling in
Reduce chaotic dependencies between UI components by introducing a mediator that centralizes communication, preventing explicit references between peers
RecipeSPA Performance: Code Splitting and Lazy Loading
Improve single-page application load times by splitting bundles at route and component level, implementing lazy loading with React.lazy and live imports
PatternIterator Pattern for Custom Collection Traversal in
Provide a way to access elements of an aggregate object sequentially without exposing its underlying representation using the Iterator pattern