Component Registry

The Component Registry is the bridge between backend UI decisions and frontend React components. It maps component identifiers to actual implementations, enabling the AI-Driven UI system.

What is the Component Registry?

The Component Registry is a centralized mapping system that connects backend UI metadata with frontend React components. When the UI Decision Layer decides to render a "BudgetForm", the Component Registry knows exactly which React component to load and how to pass props to it.

Why It's Needed

Problem Without Registry

Backend and frontend are tightly coupled. Backend must know exact component names and imports.

// Backend (BAD - hardcoded component name)
return {
  component: 'BudgetForm',  // What if renamed?
  importPath: '@fidus/ui/forms/BudgetForm'  // Coupling!
};

// Frontend (BAD - no validation)
const Component = require(response.importPath);  // Unsafe!
return <Component {...props} />;

Solution With Registry

Backend uses stable identifiers. Frontend controls component resolution and validation.

// Backend (GOOD - stable identifier)
return {
  componentType: 'budget-form',  // Stable identifier
  props: { category: 'food', amount: 500 }
};

// Frontend (GOOD - validated lookup)
const registration = registry.get('budget-form');
const Component = registration.component;
return <Component {...props} />;  // Type-safe!

Registry Structure

The registry maintains a map of component identifiers to component metadata:

interface ComponentRegistration {
  // Unique identifier (stable across versions)
  id: string;

  // Human-readable name
  name: string;

  // React component
  component: React.ComponentType<any>;

  // Zod schema for props validation
  propsSchema: z.ZodSchema;

  // Component category
  category: 'form' | 'widget' | 'card' | 'wizard';

  // Version (for backwards compatibility)
  version: string;

  // Optional description
  description?: string;
}

// Example registry
const registry = new Map<string, ComponentRegistration>([
  ['budget-form', {
    id: 'budget-form',
    name: 'Budget Form',
    component: BudgetForm,
    propsSchema: BudgetFormPropsSchema,
    category: 'form',
    version: '1.0.0',
    description: 'Form for creating and editing budgets'
  }],
  ['calendar-widget', {
    id: 'calendar-widget',
    name: 'Calendar Widget',
    component: CalendarWidget,
    propsSchema: CalendarWidgetPropsSchema,
    category: 'widget',
    version: '1.2.0',
    description: 'Interactive calendar display'
  }]
]);

How Backend Uses It

The backend (UI Decision Layer) returns stable component identifiers:

// In UI Decision Layer (Python/FastAPI)
class UIDecisionResponse:
    ui_form: str  # 'form' | 'chat' | 'widget' | 'wizard'
    component_type: str  # Stable identifier: 'budget-form'
    props: dict  # Component props
    confidence: float
    reasoning: str

# Example response
{
  "ui_form": "form",
  "component_type": "budget-form",  # ← Registry lookup key
  "props": {
    "category": "food",
    "suggested_amount": 500,
    "currency": "EUR",
    "period": "monthly"
  },
  "confidence": 0.92,
  "reasoning": "Expert user with clear intent"
}

How Frontend Uses It

The frontend looks up components and validates props:

import { componentRegistry } from '@/lib/component-registry';

interface UIResponse {
  uiForm: string;
  componentType: string;
  props: Record<string, unknown>;
}

function DynamicUIRenderer({ response }: { response: UIResponse }) {
  // 1. Look up component in registry
  const registration = componentRegistry.get(response.componentType);

  if (!registration) {
    console.error(`Component not found: ${response.componentType}`);
    return <ErrorFallback message="Unknown component" />;
  }

  // 2. Validate props with Zod schema
  const validationResult = registration.propsSchema.safeParse(response.props);

  if (!validationResult.success) {
    console.error('Invalid props:', validationResult.error);
    return <ErrorFallback message="Invalid component props" />;
  }

  // 3. Render component with validated props
  const Component = registration.component;
  return <Component {...validationResult.data} />;
}

Versioning Support

The registry supports component versioning for backwards compatibility:

Version Evolution

v1.0.0 - Initial Release
{
  id: 'budget-form',
  component: BudgetFormV1,
  propsSchema: z.object({
    category: z.string(),
    amount: z.number()
  })
}
v1.1.0 - Add Currency Support
{
  id: 'budget-form',
  component: BudgetFormV1_1,
  propsSchema: z.object({
    category: z.string(),
    amount: z.number(),
    currency: z.string().optional()  // ← Backward compatible
  })
}
v2.0.0 - Breaking Change
// Register both versions
registry.set('budget-form', BudgetFormV2Registration);
registry.set('budget-form-v1', BudgetFormV1Registration);

// Backend can specify version
{
  componentType: 'budget-form-v1',  // Legacy clients
  // or
  componentType: 'budget-form'  // Latest version
}

Type Safety with TypeScript

The registry provides full type safety using TypeScript generics:

import { z } from 'zod';

// Define props schema
const BudgetFormPropsSchema = z.object({
  category: z.string(),
  suggestedAmount: z.number().optional(),
  currency: z.enum(['EUR', 'USD', 'GBP']),
  period: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
  onSubmit: z.function().optional()
});

// Infer TypeScript type from schema
type BudgetFormProps = z.infer<typeof BudgetFormPropsSchema>;

// Component with typed props
function BudgetForm(props: BudgetFormProps) {
  // TypeScript knows all props!
  return (
    <form>
      <input name="category" defaultValue={props.category} />
      {/* ... */}
    </form>
  );
}

// Register with schema
componentRegistry.register({
  id: 'budget-form',
  name: 'Budget Form',
  component: BudgetForm,
  propsSchema: BudgetFormPropsSchema,  // ← Runtime validation
  category: 'form',
  version: '1.0.0'
});

Adding New Components

To add a new component to the registry:

  1. 1. Create the React Component
    // components/TransactionList.tsx
    interface TransactionListProps {
      transactions: Transaction[];
      onTransactionClick?: (id: string) => void;
    }
    
    export function TransactionList(props: TransactionListProps) {
      return (
        <div>
          {props.transactions.map(tx => (
            <TransactionCard key={tx.id} transaction={tx} />
          ))}
        </div>
      );
    }
  2. 2. Define Zod Schema
    // components/TransactionList.schema.ts
    import { z } from 'zod';
    
    export const TransactionListPropsSchema = z.object({
      transactions: z.array(z.object({
        id: z.string(),
        amount: z.number(),
        category: z.string(),
        date: z.string(),
        merchant: z.string().optional()
      })),
      onTransactionClick: z.function().optional()
    });
  3. 3. Register Component
    // lib/component-registry.ts
    import { TransactionList } from '@/components/TransactionList';
    import { TransactionListPropsSchema } from '@/components/TransactionList.schema';
    
    componentRegistry.register({
      id: 'transaction-list',  // ← Stable identifier
      name: 'Transaction List',
      component: TransactionList,
      propsSchema: TransactionListPropsSchema,
      category: 'widget',
      version: '1.0.0',
      description: 'List of financial transactions'
    });
  4. 4. Update Backend Constants
    # backend/constants.py
    COMPONENT_TYPES = {
        'BUDGET_FORM': 'budget-form',
        'CALENDAR_WIDGET': 'calendar-widget',
        'TRANSACTION_LIST': 'transaction-list',  # ← Add here
    }
    
    # Now backend can use:
    component_type = COMPONENT_TYPES['TRANSACTION_LIST']

Error Handling

The registry provides robust error handling:

class ComponentRegistry {
  get(id: string): ComponentRegistration | null {
    const registration = this.components.get(id);

    if (!registration) {
      console.error(`Component not found in registry: ${id}`);
      // Log to monitoring service
      logger.error('component_not_found', { componentId: id });
      return null;
    }

    return registration;
  }

  validate(id: string, props: unknown): ValidationResult {
    const registration = this.get(id);

    if (!registration) {
      return { success: false, error: 'Component not found' };
    }

    const result = registration.propsSchema.safeParse(props);

    if (!result.success) {
      console.error(`Invalid props for ${id}:`, result.error);
      // Log to monitoring service
      logger.error('invalid_component_props', {
        componentId: id,
        error: result.error
      });
      return { success: false, error: result.error };
    }

    return { success: true, data: result.data };
  }
}

Registry Implementation

// lib/component-registry.ts
import { z } from 'zod';
import type { ComponentType } from 'react';

interface ComponentRegistration {
  id: string;
  name: string;
  component: ComponentType<any>;
  propsSchema: z.ZodSchema;
  category: 'form' | 'widget' | 'card' | 'wizard';
  version: string;
  description?: string;
}

class ComponentRegistry {
  private components = new Map<string, ComponentRegistration>();

  register(registration: ComponentRegistration): void {
    if (this.components.has(registration.id)) {
      console.warn(`Component ${registration.id} already registered`);
    }
    this.components.set(registration.id, registration);
  }

  get(id: string): ComponentRegistration | null {
    return this.components.get(id) || null;
  }

  getAll(): ComponentRegistration[] {
    return Array.from(this.components.values());
  }

  getByCategory(category: string): ComponentRegistration[] {
    return this.getAll().filter(c => c.category === category);
  }
}

export const componentRegistry = new ComponentRegistry();

// Register all components
componentRegistry.register({
  id: 'budget-form',
  name: 'Budget Form',
  component: BudgetForm,
  propsSchema: BudgetFormPropsSchema,
  category: 'form',
  version: '1.0.0'
});

componentRegistry.register({
  id: 'calendar-widget',
  name: 'Calendar Widget',
  component: CalendarWidget,
  propsSchema: CalendarWidgetPropsSchema,
  category: 'widget',
  version: '1.2.0'
});

// ... more registrations

Benefits

  • Decoupling: Backend doesn't need to know React component details
  • Type Safety: Zod schemas ensure props are valid at runtime
  • Versioning: Support multiple component versions simultaneously
  • Validation: Catch invalid props before rendering
  • Centralized: Single source of truth for all UI components
  • Testable: Mock registry for testing

Testing Components

describe('ComponentRegistry', () => {
  it('should return registered component', () => {
    const registration = componentRegistry.get('budget-form');
    expect(registration).toBeDefined();
    expect(registration?.id).toBe('budget-form');
  });

  it('should validate valid props', () => {
    const result = componentRegistry.validate('budget-form', {
      category: 'food',
      suggestedAmount: 500,
      currency: 'EUR',
      period: 'monthly'
    });

    expect(result.success).toBe(true);
  });

  it('should reject invalid props', () => {
    const result = componentRegistry.validate('budget-form', {
      category: 123,  // Wrong type
      currency: 'INVALID'  // Not in enum
    });

    expect(result.success).toBe(false);
  });

  it('should return null for unknown component', () => {
    const registration = componentRegistry.get('unknown-component');
    expect(registration).toBeNull();
  });
});

Related Documentation