Front-end Engineering Lab
PatternsArchitecture Patterns

Registry Pattern

Dynamic type-to-component mapping - commonly used for rendering components based on data types, plugin systems, and dynamic form fields

The Registry Pattern provides a centralized way to store and retrieve objects by key. In front-end development, it's commonly used for dynamic type-to-component mapping, plugin systems, and rendering components based on data types.

🎯 The Problem

// ❌ BAD: Hard-coded type checking
function renderField(type: string, props: unknown) {
  if (type === 'text') {
    return <TextInput {...props} />;
  } else if (type === 'email') {
    return <EmailInput {...props} />;
  } else if (type === 'number') {
    return <NumberInput {...props} />;
  }
  // More types = more if/else 😱
  return null;
}

// ✅ GOOD: Registry Pattern
class ComponentRegistry {
  private components = new Map<string, React.ComponentType<unknown>>();
  
  register<T>(type: string, component: React.ComponentType<T>) {
    this.components.set(type, component as React.ComponentType<unknown>);
  }
  
  get(type: string): React.ComponentType<unknown> | undefined {
    return this.components.get(type);
  }
  
  render(type: string, props: unknown) {
    const Component = this.get(type);
    if (!Component) return null;
    return <Component {...(props as Record<string, unknown>)} />;
  }
}

const registry = new ComponentRegistry();
registry.register('text', TextInput);
registry.register('email', EmailInput);
registry.register('number', NumberInput);

function renderField(type: string, props: unknown) {
  return registry.render(type, props);
}

📚 Common Front-End Examples

1. Dynamic Form Field Rendering

import React, { useState } from 'react';

interface FieldConfig {
  type: string;
  name: string;
  label: string;
  placeholder?: string;
  required?: boolean;
  validation?: (value: string) => boolean;
}

interface TextFieldProps {
  name: string;
  label: string;
  placeholder?: string;
  value: string;
  onChange: (value: string) => void;
}

interface NumberFieldProps {
  name: string;
  label: string;
  placeholder?: string;
  value: number;
  onChange: (value: number) => void;
  min?: number;
  max?: number;
}

function TextField({ name, label, placeholder, value, onChange }: TextFieldProps) {
  return (
    <div>
      <label>{label}</label>
      <input
        type="text"
        name={name}
        placeholder={placeholder}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    </div>
  );
}

function NumberField({ name, label, placeholder, value, onChange, min, max }: NumberFieldProps) {
  return (
    <div>
      <label>{label}</label>
      <input
        type="number"
        name={name}
        placeholder={placeholder}
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
        min={min}
        max={max}
      />
    </div>
  );
}

function EmailField({ name, label, placeholder, value, onChange }: TextFieldProps) {
  return (
    <div>
      <label>{label}</label>
      <input
        type="email"
        name={name}
        placeholder={placeholder}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    </div>
  );
}

class FieldRegistry {
  private fields = new Map<string, React.ComponentType<unknown>>();
  
  register<T>(type: string, component: React.ComponentType<T>) {
    this.fields.set(type, component as React.ComponentType<unknown>);
  }
  
  get(type: string): React.ComponentType<unknown> | undefined {
    return this.fields.get(type);
  }
  
  render(type: string, props: unknown) {
    const Field = this.get(type);
    if (!Field) {
      console.warn(`Field type "${type}" not registered`);
      return null;
    }
    return <Field {...(props as Record<string, unknown>)} />;
  }
}

const fieldRegistry = new FieldRegistry();
fieldRegistry.register('text', TextField);
fieldRegistry.register('number', NumberField);
fieldRegistry.register('email', EmailField);

// Usage
function DynamicForm({ fields }: { fields: FieldConfig[] }) {
  const [formData, setFormData] = useState<Record<string, string | number>>({});
  
  const handleChange = (name: string, value: string | number) => {
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  return (
    <form>
      {fields.map(field => {
        const Field = fieldRegistry.get(field.type);
        if (!Field) return null;
        
        return (
          <Field
            key={field.name}
            name={field.name}
            label={field.label}
            placeholder={field.placeholder}
            value={formData[field.name] || (field.type === 'number' ? 0 : '')}
            onChange={(value: string | number) => handleChange(field.name, value)}
            {...(field.type === 'number' && { min: 0, max: 100 })}
          />
        );
      })}
    </form>
  );
}

function App() {
  const formFields: FieldConfig[] = [
    { type: 'text', name: 'firstName', label: 'First Name' },
    { type: 'text', name: 'lastName', label: 'Last Name' },
    { type: 'email', name: 'email', label: 'Email' },
    { type: 'number', name: 'age', label: 'Age' }
  ];
  
  return <DynamicForm fields={formFields} />;
}

// Example usage in a real app
function RegistrationForm() {
  return (
    <App />
  );
}

2. Content Type to Component Mapping

import React, { useState, useEffect } from 'react';

interface ContentItem {
  type: string;
  id: string;
  data: Record<string, unknown>;
}

interface TextContentProps {
  content: string;
}

interface ImageContentProps {
  src: string;
  alt: string;
}

interface VideoContentProps {
  src: string;
  poster?: string;
}

function TextContent({ content }: TextContentProps) {
  return <div className="text-content">{content}</div>;
}

function ImageContent({ src, alt }: ImageContentProps) {
  return <img src={src} alt={alt} className="image-content" />;
}

function VideoContent({ src, poster }: VideoContentProps) {
  return (
    <video src={src} poster={poster} controls className="video-content">
      Your browser does not support video.
    </video>
  );
}

class ContentTypeRegistry {
  private components = new Map<string, React.ComponentType<Record<string, unknown>>>();
  
  register<T extends Record<string, unknown>>(
    type: string,
    component: React.ComponentType<T>
  ) {
    this.components.set(type, component as React.ComponentType<Record<string, unknown>>);
  }
  
  get(type: string): React.ComponentType<Record<string, unknown>> | undefined {
    return this.components.get(type);
  }
  
  render(item: ContentItem) {
    const Component = this.get(item.type);
    if (!Component) {
      return <div>Unknown content type: {item.type}</div>;
    }
    return <Component {...item.data} />;
  }
}

const contentTypeRegistry = new ContentTypeRegistry();
contentTypeRegistry.register('text', TextContent);
contentTypeRegistry.register('image', ImageContent);
contentTypeRegistry.register('video', VideoContent);

// Usage
function ContentRenderer({ items }: { items: ContentItem[] }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {contentTypeRegistry.render(item)}
        </div>
      ))}
    </div>
  );
}

function Feed() {
  const [content, setContent] = useState<ContentItem[]>([]);
  
  useEffect(() => {
    // Simulate fetching content
    setContent([
      {
        type: 'text',
        id: '1',
        data: { content: 'This is a text post' }
      },
      {
        type: 'image',
        id: '2',
        data: { src: '/image.jpg', alt: 'An image' }
      },
      {
        type: 'video',
        id: '3',
        data: { src: '/video.mp4', poster: '/poster.jpg' }
      }
    ]);
  }, []);
  
  return <ContentRenderer items={content} />;
}

3. Widget Registry System

import React, { useState, useEffect } from 'react';

interface WidgetConfig {
  type: string;
  id: string;
  title: string;
  props: Record<string, unknown>;
}

interface ChartWidgetProps {
  data: number[];
  title: string;
}

interface TableWidgetProps {
  columns: string[];
  rows: Array<Record<string, string>>;
  title: string;
}

interface StatsWidgetProps {
  value: number;
  label: string;
  title: string;
}

function ChartWidget({ data, title }: ChartWidgetProps) {
  return (
    <div className="widget chart">
      <h3>{title}</h3>
      <div>{/* Chart rendering logic */}</div>
    </div>
  );
}

function TableWidget({ columns, rows, title }: TableWidgetProps) {
  return (
    <div className="widget table">
      <h3>{title}</h3>
      <table>
        <thead>
          <tr>
            {columns.map(col => (
              <th key={col}>{col}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((row, i) => (
            <tr key={i}>
              {columns.map(col => (
                <td key={col}>{String(row[col] || '')}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function StatsWidget({ value, label, title }: StatsWidgetProps) {
  return (
    <div className="widget stats">
      <h3>{title}</h3>
      <div className="value">{value}</div>
      <div className="label">{label}</div>
    </div>
  );
}

class WidgetRegistry {
  private widgets = new Map<string, React.ComponentType<Record<string, unknown>>>();
  
  register<T extends Record<string, unknown>>(
    type: string,
    widget: React.ComponentType<T>
  ) {
    this.widgets.set(type, widget as React.ComponentType<Record<string, unknown>>);
  }
  
  get(type: string): React.ComponentType<Record<string, unknown>> | undefined {
    return this.widgets.get(type);
  }
  
  render(config: WidgetConfig) {
    const Widget = this.get(config.type);
    if (!Widget) {
      return <div>Unknown widget type: {config.type}</div>;
    }
    return <Widget {...config.props} title={config.title} />;
  }
}

const widgetRegistry = new WidgetRegistry();
widgetRegistry.register('chart', ChartWidget);
widgetRegistry.register('table', TableWidget);
widgetRegistry.register('stats', StatsWidget);

// Usage
function Dashboard({ widgets }: { widgets: WidgetConfig[] }) {
  return (
    <div className="dashboard">
      {widgets.map(widget => (
        <div key={widget.id}>
          {widgetRegistry.render(widget)}
        </div>
      ))}
    </div>
  );
}

function App() {
  const [widgets, setWidgets] = useState<WidgetConfig[]>([]);
  
  useEffect(() => {
    // Load widget configuration from API
    setWidgets([
      {
        type: 'stats',
        id: '1',
        title: 'Total Users',
        props: { value: 1234, label: 'Active Users' }
      },
      {
        type: 'chart',
        id: '2',
        title: 'Sales Chart',
        props: { data: [10, 20, 30, 40, 50] }
      },
      {
        type: 'table',
        id: '3',
        title: 'Recent Orders',
        props: {
          columns: ['Order ID', 'Customer', 'Amount'],
          rows: [
            { 'Order ID': '123', 'Customer': 'John', 'Amount': '$100' },
            { 'Order ID': '124', 'Customer': 'Jane', 'Amount': '$200' }
          ]
        }
      }
    ]);
  }, []);
  
  return <Dashboard widgets={widgets} />;
}

4. Plugin System Registry

import React, { useEffect } from 'react';

interface Plugin {
  name: string;
  type: string;
  component: React.ComponentType<Record<string, unknown>>;
  config?: Record<string, unknown>;
}

class PluginRegistry {
  private plugins = new Map<string, Plugin>();
  
  register(plugin: Plugin) {
    this.plugins.set(plugin.name, plugin);
  }
  
  get(name: string): Plugin | undefined {
    return this.plugins.get(name);
  }
  
  getByType(type: string): Plugin[] {
    return Array.from(this.plugins.values()).filter(p => p.type === type);
  }
  
  render(name: string, props?: Record<string, unknown>) {
    const plugin = this.get(name);
    if (!plugin) {
      return <div>Plugin "{name}" not found</div>;
    }
    const Component = plugin.component;
    return <Component {...plugin.config} {...props} />;
  }
}

const pluginRegistry = new PluginRegistry();

// Initialize plugins on app start
pluginRegistry.register({
  name: 'analytics',
  type: 'tracking',
  component: () => <div>Analytics Plugin</div>
});

pluginRegistry.register({
  name: 'chat',
  type: 'communication',
  component: ({ userId }: { userId?: string }) => (
    <div>Chat Plugin {userId ? `for user ${userId}` : ''}</div>
  )
});

// Usage
function App() {
  const trackingPlugins = pluginRegistry.getByType('tracking');
  
  return (
    <div>
      <h1>My App</h1>
      {trackingPlugins.map(plugin => (
        <div key={plugin.name}>
          {pluginRegistry.render(plugin.name, { userId: '123' })}
        </div>
      ))}
    </div>
  );
}

5. Notification Type Registry

import React, { useState } from 'react';

interface Notification {
  type: string;
  id: string;
  message: string;
  props?: Record<string, unknown>;
}

interface SuccessNotificationProps {
  message: string;
  onDismiss: () => void;
}

interface ErrorNotificationProps {
  message: string;
  error?: Error;
  onDismiss: () => void;
}

interface WarningNotificationProps {
  message: string;
  action?: () => void;
  onDismiss: () => void;
}

function SuccessNotification({ message, onDismiss }: SuccessNotificationProps) {
  return (
    <div className="notification success">
      <span>✓ {message}</span>
      <button onClick={onDismiss}>×</button>
    </div>
  );
}

function ErrorNotification({ message, error, onDismiss }: ErrorNotificationProps) {
  return (
    <div className="notification error">
      <span>✗ {message}</span>
      {error && <details>{error.message}</details>}
      <button onClick={onDismiss}>×</button>
    </div>
  );
}

function WarningNotification({ message, action, onDismiss }: WarningNotificationProps) {
  return (
    <div className="notification warning">
      <span>⚠ {message}</span>
      {action && <button onClick={action}>Action</button>}
      <button onClick={onDismiss}>×</button>
    </div>
  );
}

class NotificationRegistry {
  private types = new Map<string, React.ComponentType<Record<string, unknown>>>();
  
  register<T extends Record<string, unknown>>(
    type: string,
    component: React.ComponentType<T>
  ) {
    this.types.set(type, component as React.ComponentType<Record<string, unknown>>);
  }
  
  get(type: string): React.ComponentType<Record<string, unknown>> | undefined {
    return this.types.get(type);
  }
  
  render(notification: Notification, onDismiss: () => void) {
    const Component = this.get(notification.type);
    if (!Component) {
      return <div>Unknown notification type: {notification.type}</div>;
    }
    return (
      <Component
        message={notification.message}
        onDismiss={onDismiss}
        {...notification.props}
      />
    );
  }
}

const notificationRegistry = new NotificationRegistry();
notificationRegistry.register('success', SuccessNotification);
notificationRegistry.register('error', ErrorNotification);
notificationRegistry.register('warning', WarningNotification);

// Usage
function NotificationContainer() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  
  const showNotification = (notification: Notification) => {
    setNotifications(prev => [...prev, notification]);
  };
  
  const dismissNotification = (id: string) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };
  
  // Example: Show notifications
  useEffect(() => {
    showNotification({
      type: 'success',
      id: '1',
      message: 'Operation completed successfully!'
    });
  }, []);
  
  return (
    <div className="notifications">
      {notifications.map(notification => (
        <div key={notification.id}>
          {notificationRegistry.render(notification, () => dismissNotification(notification.id))}
        </div>
      ))}
    </div>
  );
}

🎯 When to Use

This pattern is commonly used and recommended for:

  1. Dynamic component rendering - Render components based on data types or configuration
  2. Form builders - Dynamic form generation based on field types
  3. Plugin systems - Register and render plugins dynamically
  4. Content management - Render different content types (text, image, video)
  5. Widget systems - Dashboard widgets that can be added/removed dynamically

📚 Key Takeaways

  • Centralized mapping - Single place to manage type-to-component relationships
  • Extensibility - Add new types without modifying existing code
  • Decoupling - Components don't need to know about all possible types
  • Dynamic behavior - Runtime component selection based on data
  • Maintainability - Easy to add, remove, or modify registered components

On this page