Extensions and Extension Points
Extensions provide dynamic UI extensibility through a plug-and-slot system built on the service provider/consumer pattern. This architecture enables building highly modular applications where UI components can be added, removed, or modified at runtime without affecting the core system structure.
Core Concepts
The extension system operates through two complementary mechanisms:
- Extension Points — Slots or placeholders that define where new UI elements should render (technically, UI service consumers)
- Extensions — Plugs that provide new UI elements for dynamic injection into corresponding placeholders (technically, UI service providers)
This service-based approach transforms the standard UI paradigm where each element explicitly defines its complete visual hierarchy. Instead, components can dynamically discover and render content provided by other modules at runtime.
Architecture Benefits
- Runtime Modularity: Components can be added or removed without code changes
- Loose Coupling: Extension providers don’t need direct references to extension points
- Plugin Architecture: Third-party modules can extend core functionality
- Dynamic Composition: UI structure adapts based on available extensions
React Implementation
Here’s how to implement the extension pattern using React hooks:
import { newContextService } from "@/utils/services.js";
import { useState, useEffect } from "react";
type ProcessContext = Record<string, unknown>;
interface MenuItem {
id: string;
label: string;
action: () => void;
}
// Create a menu item service
const [consumeMenuItems, newMenuItemExtension] = newContextService<MenuItem, ProcessContext>(
"ui.menu.items",
(context) => context.parent
);
// Extension Point Hook - consumes menu items
function useMenuItems(context: ProcessContext): MenuItem[] {
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
useEffect(() => {
// Subscribe to dynamic menu items
return consumeMenuItems(context, setMenuItems);
}, [context]);
return menuItems;
}
// Extension Hook - provides menu items
function useMenuItemExtension(context: ProcessContext, item: MenuItem) {
useEffect(() => {
const [provideItem, removeItem] = newMenuItemExtension(context);
provideItem(item);
return removeItem;
}, [context, item]);
}
// Extension Point Component - renders all available menu items
function MainMenu({ context }: { context: ProcessContext }) {
const menuItems = useMenuItems(context);
return (
<nav>
{menuItems.map(item => (
<button key={item.id} onClick={item.action}>
{item.label}
</button>
))}
</nav>
);
}
// Extension Provider Component - contributes menu items
function MyDashboard({ context }: { context: ProcessContext }) {
useMenuItemExtension(context, {
id: "settings",
label: "Settings",
action: () => navigate("/settings")
});
useMenuItemExtension(context, {
id: "profile",
label: "Profile",
action: () => navigate("/profile")
});
return (
<div>
<h1>Dashboard</h1>
{/* Dashboard content */}
</div>
);
}
Advanced Patterns
Conditional Extensions
Extensions can be conditionally registered based on application state:
function ConditionalExtension({ context, user }: { context: ProcessContext, user: User }) {
useEffect(() => {
if (user.role === 'admin') {
const [provideAdminItem, removeAdminItem] = newMenuItemExtension(context);
provideAdminItem({
id: "admin-panel",
label: "Admin Panel",
action: () => navigate("/admin")
});
return removeAdminItem;
}
}, [context, user.role]);
return null;
}
Typed Extension Points
Use TypeScript generics for type-safe extensions:
interface ToolbarAction {
id: string;
icon: string;
tooltip: string;
handler: () => void;
position?: 'left' | 'right';
}
const [consumeToolbarActions, newToolbarActionExtension] = newContextService<
ToolbarAction,
ProcessContext
>("ui.toolbar.actions", getParentContext);
function useToolbarExtension(context: ProcessContext, action: ToolbarAction) {
useEffect(() => {
const [provideAction, removeAction] = newToolbarActionExtension(context);
provideAction(action);
return removeAction;
}, [context, action]);
}
Extension Priorities
Implement ordering for extensions:
interface PrioritizedExtension {
id: string;
priority: number;
component: React.ComponentType;
}
function usePrioritizedExtensions(context: ProcessContext): PrioritizedExtension[] {
const [extensions, setExtensions] = useState<PrioritizedExtension[]>([]);
useEffect(() => {
return consumeExtensions(context, (rawExtensions) => {
const sorted = rawExtensions.sort((a, b) => b.priority - a.priority);
setExtensions(sorted);
});
}, [context]);
return extensions;
}
StateWalker Integration
Extensions work seamlessly with StateWalker’s state machine orchestrator:
// State handler that provides extensions
function FeatureAState(context: ProcessContext) {
const [register, cleanup] = newRegistry();
// Register extension when entering state
const [provideExtension, removeExtension] = newFeatureExtension(context);
register(removeExtension);
provideExtension({
id: "feature-a-tool",
label: "Feature A Tool",
action: () => activateFeatureA()
});
// Clean up when leaving state
return cleanup;
}
// Extension point that adapts to state changes
function DynamicToolbar({ context }: { context: ProcessContext }) {
const tools = useToolExtensions(context);
return (
<div className="toolbar">
{tools.map(tool => (
<ToolButton key={tool.id} tool={tool} />
))}
</div>
);
}
Best Practices
1. Namespace Extension Keys
Use descriptive, hierarchical naming for extension services:
// Good: clear, hierarchical naming
const [consumeMenuItems, newMenuItemExtension] = newContextService("ui.menu.main.items", getParentContext);
const [consumeSidebarWidgets, newSidebarWidgetExtension] = newContextService("ui.sidebar.widgets", getParentContext);
2. Handle Empty States
Gracefully handle cases where no extensions are available:
function ExtensibleContainer({ context }: { context: ProcessContext }) {
const extensions = useExtensions(context);
if (extensions.length === 0) {
return <div className="empty-state">No extensions available</div>;
}
return (
<div>
{extensions.map(ext => <ExtensionComponent key={ext.id} extension={ext} />)}
</div>
);
}
3. Cleanup Management
Always clean up extension registrations:
function ExtensionProvider({ context, extensions }: Props) {
useEffect(() => {
const cleanupFunctions = extensions.map(ext => {
const [provide, remove] = newExtension(context);
provide(ext);
return remove;
});
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
};
}, [context, extensions]);
}
Related Patterns
- Services — Core provider/consumer mechanism
- Registry — Cleanup function management
- Listeners — Event-based extension coordination
Use Cases
- Plugin Systems: Third-party modules extending core functionality
- Dashboard Widgets: Dynamic widget registration and rendering
- Toolbar Extensions: Context-sensitive tool registration
- Menu Systems: Dynamic menu item contribution
- Content Areas: Pluggable content sections
- Form Fields: Dynamic form extension capabilities