When your React application grows beyond a few feature modules, you begin to feel pain:
• Components become too tightly coupled
• Teams accidentally overwrite each other’s UI behavior
• Feature releases require risky regression cycles
• Reusable components… aren’t really reusable
• Code reviews turn into debates on folder structure
As UI Architects, our job is to design UI systems that scale, not just UI screens.
One of the most powerful architectural approaches I’ve used in large-scale frontend apps (50+ developers, multi-year lifespan) is the UI Composition Architecture.
This post breaks down the pattern with concepts, diagrams, examples, and real-world React code you can try immediately.
What Problem Does UI Composition Solve?
Traditional React architecture looks like this:
Pages → Feature Modules → Widgets → Components → UI Elements
Works great… until:
• Two teams need to extend the same screen
• A feature needs to plug into multiple pages
• A global rule (e.g., dark theme, tracking, A/B test) must wrap UI without modifying every page
• You need to ship new UI without touching “core” code
UI Composition flips the mental model.
Instead of pages owning components, components own behaviors, and pages compose them.
⸻
The Core Idea: Behavior is a Plugin, Not a Hardcoded Feature
Instead of a giant “God Page,” you have:
Base Layout + Registrable UI Slots
┌─────────────────────────────┐
│ Page Layout │
│ ┌──────────┐ ┌──────────┐ │
│ │ Slot A │ │ Slot B │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ Slot C │ │
│ └──────────┘ │
└─────────────────────────────┘
↓ Features register ↓
Feature 1 → Slot A
Feature 2 → Slot B
Feature 3 → Slot C
This makes UI extensible, testable, and team-friendly.
⸻
A Real Example: Pluggable Dashboard Widgets in React
Consider a dashboard where different teams own different widgets.
- Define a Slot Component
// framework/Slot.js
export const Slot = ({ name, registry }) => {
const Component = registry[name];
return Component ? : null;
};
- Create a Central Widget Registry
// framework/registry.js
export const widgetRegistry = {};
export const registerWidget = (name, component) => {
widgetRegistry[name] = component;
};
- The Dashboard Layout Uses Slots
// pages/Dashboard.jsx
import { Slot } from "../framework/Slot";
import { widgetRegistry } from "../framework/registry";
export default function Dashboard() {
return (
<div className="right-panel">
<Slot name="profile" registry={widgetRegistry} />
<Slot name="trends" registry={widgetRegistry} />
</div>
</div>
);
}
- Teams Independently Register New Widgets
Team weather:
// features/weather/index.js
import { registerWidget } from "../../framework/registry";
const WeatherWidget = () => (
);
registerWidget("weather", WeatherWidget);
Team trends:
// features/trends/index.js
import { registerWidget } from "../../framework/registry";
const TrendsWidget = () => (
);
registerWidget("trends", TrendsWidget);
- Result?
Without modifying Dashboard.jsx, you can add:
• A new widget
• A new behavior
• A different version (A/B testing)
• User-based personalization
This pattern scales incredibly well in real enterprise applications.
⸻
🧠 Why This Pattern Works (Architectural Advantages)
✔ Zero coupling
Features plug into slots — no imports, no dependencies, no cross-team breaking changes.
✔ Continuous Delivery
Teams ship independently without touching layout code.
✔ Powerful Customization
Just register different components per tenant, role, or feature flag.
✔ Perfect for Micro-Frontends
Each widget can even be loaded as a federated module (Module Federation).
✔ Enables “Dynamic UI”
UI becomes a dynamic system, not a static tree.
⸻
Optional: Slot with Props Injection
A more advanced slot allows the layout to pass context:
export const Slot = ({ name, registry, props }) => {
const Component = registry[name];
return Component ? : null;
};
Now we can do:
Widget:
const WeatherWidget = ({ city }) => (
);
Real-World Use Cases You Can Implement
Role-based UI
• Admin sees 6 widgets
• User sees 3
• Guest sees only 1
No branching logic on the page.Marketplace Extensibility
Plugins from 3rd-party developers.
- Experimentation (A/B Testing)
Register the experiment version based on a flag.
- Theming
Slots can even swap different component trees for entire themes.
- Multi-tenant SaaS
Different clients get different UI behavior.
⸻
Final Thoughts
As UI Architects, our goal is to build systems that:
• allow rapid iteration
• scale organizationally
• remain flexible for years
• empower teams to build without stepping on each other
UI Composition Architecture is one of the most pragmatic and powerful ways to achieve that.
If your app feels “rigid” or “hard to extend,” try introducing Slots + Registries into your architecture — your future self (and your developers) will thank you.
Top comments (0)