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:
- Dynamic component rendering - Render components based on data types or configuration
- Form builders - Dynamic form generation based on field types
- Plugin systems - Register and render plugins dynamically
- Content management - Render different content types (text, image, video)
- 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