Structural Patterns Meet Component Composition
Exploring how Decorator, Adapter, Facade, Composite, and Proxy patterns evolved in React and TypeScript. Learn when HOCs give way to hooks, how adapters isolate third-party APIs, and when facades simplify complexity.
Structural patterns organize relationships between objects and classes. The Gang of Four documented Decorator, Adapter, Facade, Composite, and Proxy in 1994 for C++ and Smalltalk. In modern TypeScript and React ecosystems, these patterns haven't disappeared - they've been absorbed into framework conventions, hooks, and type-safe wrappers.
This post examines how structural patterns manifest in React component composition, when higher-order components still matter, and how TypeScript's type system enhances classical implementations.
Decorator Pattern: Three Meanings in TypeScript
The term "decorator" means three different things in TypeScript ecosystems:
- Gang of Four Decorator Pattern: Adding behavior to objects dynamically
- React Higher-Order Components (HOCs): Enhancing components with additional functionality
- TypeScript Decorator Syntax: Stage 3 proposal for class/method decorators
Each solves different problems. Let's examine when each approach adds value.
React HOCs: The Classic Decorator Implementation
Higher-order components enhance components by wrapping them with additional functionality:
This works, but HOCs create problems:
Wrapper Hell: Stacking multiple HOCs creates deeply nested component trees:
Props Collision: Multiple HOCs might inject props with the same names, causing conflicts.
Ref Forwarding Complexity: Passing refs through HOC layers requires explicit forwarding. Note that React 19 deprecated forwardRef - refs can now be passed as standard props, simplifying this pattern.
Unclear Data Flow: Props injected by HOCs aren't visible in component signature.
Modern Alternative: Custom Hooks
Hooks provide the same functionality with cleaner composition:
Hooks eliminate wrapper hell, make data flow explicit, and compose naturally. Each hook's functionality is clear from its return values.
When HOCs Still Make Sense
Despite hooks' advantages, HOCs remain valuable for:
Library Code Where Hooks Can't Be Used: Some contexts require wrapping components without modifying their internals.
Visual Wrappers: HOCs that add purely visual elements around components:
Legacy Class Component Integration: When migrating from class components, HOCs bridge the gap to modern hooks.
TypeScript Decorator Syntax
TypeScript decorators (Stage 3 proposal, supported in TypeScript 5.0+) enable declarative metadata and behavior modification:
TypeScript decorators work well for:
- Logging and monitoring
- Validation and authorization
- Caching and memoization
- Performance tracking
Real Scenario: Analytics Tracking
Consider adding analytics to multiple components. Let's compare approaches:
HOC Approach (verbose):
Hook Approach (cleaner):
The hook approach integrates naturally into component logic, avoiding extra wrapper components and making the tracking explicit in the component body.
Adapter Pattern: Bridging Incompatible Interfaces
Adapters translate one interface to another. In TypeScript, they isolate third-party APIs, enabling easier testing and future migrations.
The Problem: Third-Party API Mismatch
External libraries often have interfaces that don't match your domain model:
These representations serve different purposes. Stripe's API is optimized for their backend, your domain model for your business logic.
The Solution: Adapter Layer
Create adapters to translate between representations:
When Stripe changes their API, only the adapter needs updating. Your domain model remains stable.
TypeScript Mapped Types as Adapters
TypeScript's type system enables compile-time adapters using mapped types:
This pattern works well when many APIs share similar wrapper structures.
Adapter for React Component Props
Adapters help integrate third-party UI libraries into your design system:
This isolation means switching from Material-UI to another library only requires updating the adapter component, not every usage site.
When to Create Adapters
Create adapters for:
- Third-party services you might replace (payment processors, cloud providers)
- APIs with poor TypeScript support
- External services requiring complex domain model transformations
- Libraries undergoing frequent breaking changes
Don't create adapters for:
- Stable, well-typed libraries (lodash, date-fns)
- Internal utilities under your control
- Simple one-to-one mappings (use utility functions instead)
Facade Pattern: Simplifying Complex Subsystems
Facades provide simplified interfaces to complex subsystems. In TypeScript, they hide initialization complexity, coordinate multiple services, and reduce coupling to implementation details.
The Problem: Complex API Setup
The AWS SDK v3 requires specific configuration for each service, command objects, and careful error handling:
Each operation requires knowledge of AWS SDK conventions, command objects, and data marshalling.
The Solution: Unified Facade
Create a facade that provides domain-specific operations:
The facade hides AWS SDK complexity behind domain-appropriate methods. Consumers work with your application's vocabulary, not AWS's.
Facade for Complex Form Operations
Forms often involve validation, submission, error handling, and analytics:
The facade coordinates multiple subsystems - API client, analytics, form state - behind a single method call.
Barrel Exports: A Controversial Facade
Barrel exports (index.ts files) create public facades for modules:
This hides internal structure, letting you refactor internals without breaking consumers.
However: 2024 research from Atlassian shows barrel exports can harm build performance significantly. They reduced build times by 75% after removing barrel files. Next.js pages that imported 11k modules through barrels dropped to 3.5k direct imports (68% reduction).
Recommendation: Use barrel exports only for public library APIs. Avoid them for internal project modules where build performance matters.
When Facades Add Value
Create facades when:
- Coordinating multiple services for common operations
- Simplifying complex initialization sequences
- Providing domain-specific interfaces to technical APIs
- Isolating applications from third-party API changes
Don't create facades for:
- Simple object creation (that's just unnecessary indirection)
- Wrapping single methods with no additional logic
- Internal utilities that are already simple
Composite Pattern: React's Natural Structure
The Composite pattern treats individual objects and compositions uniformly. React's component model is inherently composite - components can contain other components, and both are treated the same way.
Classic Composite Pattern
The textbook example involves hierarchical structures like file systems:
This works, but feels verbose for React. The pattern is sound - treating individual items and collections uniformly - but the implementation doesn't leverage React's natural composition.
Modern React Approach
React components are composite by nature. Here's the same logic with idiomatic React:
This leverages React's natural recursion and discriminated unions for type safety. The composite nature emerges from component structure rather than explicit classes.
Compound Components Pattern
The compound component pattern provides flexible APIs with implicit state sharing:
This pattern appears in Radix UI, Headless UI, and Reach UI. It combines composite structure with context-based state sharing for flexible, composable APIs.
Proxy Pattern: Lazy Loading and Access Control
Proxies control access to objects, adding behavior like lazy loading, caching, validation, or access control. TypeScript provides both class-based proxies and JavaScript's Proxy API for runtime interception.
React.lazy and Suspense
React's built-in lazy loading is a proxy pattern - it intercepts component rendering to load code on demand:
React.lazy intercepts component rendering, loading the module only when needed. The Suspense boundary provides loading state while the proxy fetches code.
Custom Proxy for API Caching
Proxies add caching layers without changing calling code:
The proxy implements the same interface as the real client, adding caching transparently. Calling code doesn't know whether it's using cached or fresh data.
JavaScript Proxy API for Validation
JavaScript's Proxy API enables runtime interception:
The Proxy intercepts property access and assignment, enforcing validation rules at runtime. This works well for configuration objects or domain entities requiring invariants.
React Query as Proxy Pattern
React Query acts as a proxy for data fetching, managing caching, loading states, and refetching:
React Query intercepts data fetching, adding:
- Automatic caching with configurable TTL
- Loading and error states
- Background refetching
- Cache invalidation
- Request deduplication
The component treats React Query's useQuery as a simple data fetch, but the proxy handles complex caching and synchronization logic.
Proxy for Access Control
Proxies can enforce authorization:
The proxy enforces authorization and logging without modifying the real implementation.
Common Pitfalls and Lessons
Pitfall 1: HOC Wrapper Hell
Problem: Stacking multiple HOCs creates component trees dozens of layers deep, making debugging difficult and performance monitoring challenging.
Solution: Migrate to hooks for most cross-cutting concerns. Reserve HOCs for legacy integration or truly visual wrappers.
Pitfall 2: Leaky Facades
Problem: Facades that expose implementation details defeat their purpose.
Bad Example:
Good Example:
Lesson: Facades should speak your domain language, not the underlying library's.
Pitfall 3: Adapter Proliferation
Problem: Creating adapters for every external dependency creates maintenance burden without clear benefits.
When to create adapters:
- Dependencies you plan to swap (different cloud providers, payment processors)
- APIs with poor TypeScript support requiring type safety layers
- External services requiring significant domain model transformation
When NOT to create adapters:
- Stable, well-typed libraries (lodash, date-fns)
- Internal utilities under your control
- Simple one-to-one mappings (use utility functions)
Pitfall 4: Composite Overengineering
Problem: Implementing full composite pattern for simple component hierarchies.
Overkill:
Better:
Lesson: React's component model already provides composite behavior. Explicit pattern implementation is rarely needed.
Pitfall 5: Barrel Export Performance
Problem: Using barrel exports (index.ts files) for internal modules harms build performance.
Research: Atlassian reduced build times by 75% after removing barrel files. Next.js applications saw module imports drop from 11k to 3.5k (68% reduction).
Recommendation: Use barrel exports only for public library APIs. For internal modules, import directly:
Key Takeaways
HOCs → Hooks: Higher-order components (the decorator pattern) have been largely replaced by hooks for cleaner composition. HOCs remain useful for legacy code and purely visual wrappers.
Adapter for Boundaries: Use adapters at system boundaries - external APIs, third-party libraries - to isolate changes and maintain clean domain models. Don't adapt stable, well-typed dependencies.
Facade for Complexity: Facades excel at coordinating multiple subsystems or simplifying complex initialization. Avoid creating facades for simple object creation - that's unnecessary indirection.
Composite Is React's Nature: React's component model is inherently composite. Explicit composite pattern implementation is rarely needed - leverage React's natural composition.
Proxy for Cross-Cutting Concerns: Use proxies for caching, lazy loading, access control, and validation. JavaScript's Proxy API and patterns like React Query enable powerful interception without modifying original implementations.
Barrel Exports Are Controversial: Use sparingly for public library APIs only. Internal barrel exports harm build performance significantly.
TypeScript Enhances Patterns: Mapped types, conditional types, and discriminated unions make structural patterns more powerful and type-safe than classical implementations.
Composition Over Decoration: Modern React favors composition patterns (hooks, compound components, render props) over decoration patterns (HOCs) for better readability and maintainability.
The structural patterns from 1994 haven't disappeared. They've evolved into framework conventions, type system features, and modern architectural approaches. Understanding the underlying principles - wrapping behavior, adapting interfaces, simplifying complexity, treating parts and wholes uniformly, controlling access - helps you recognize these patterns in modern codebases and apply them appropriately when building new systems.
References
- typescriptlang.org - TypeScript Handbook and language reference.
- github.com - TypeScript project wiki (FAQ and design notes).
- react.dev - React official documentation.
- developer.mozilla.org - MDN Web Docs (web platform reference).
- semver.org - Semantic Versioning specification.
- ietf.org - IETF RFC index (protocol standards).
- arxiv.org - arXiv software engineering recent submissions (research context).
- cheatsheetseries.owasp.org - OWASP Cheat Sheet Series (applied security guidance).
Modern Perspective on Classic Design Patterns
A comprehensive series examining how classic Gang of Four design patterns have evolved in modern TypeScript, React, and functional programming contexts. Learn when classic patterns still apply, when they've been superseded, and how to recognize underlying principles in modern codebases.