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:

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

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]);
}

Use Cases