PatternsArchitecture Patterns
Factory Pattern
A function that decides which object to create - commonly used for API service instances or different types of Modals/Toasts
The Factory Pattern provides an interface for creating objects without specifying their exact classes. In front-end development, it's commonly used to create instances of API services or different types of Modals/Toasts.
🎯 The Problem
// ❌ BAD: Direct creation with conditionals
function showNotification(type: string, message: string) {
if (type === 'success') {
return <SuccessToast message={message} />;
} else if (type === 'error') {
return <ErrorToast message={message} />;
} else if (type === 'warning') {
return <WarningToast message={message} />;
}
// More types = more if/else 😱
}
// ✅ GOOD: Factory Pattern
interface Toast {
render(): React.ReactNode;
}
class SuccessToast implements Toast {
constructor(private message: string) {}
render() {
return <div className="toast success">{this.message}</div>;
}
}
class ErrorToast implements Toast {
constructor(private message: string) {}
render() {
return <div className="toast error">{this.message}</div>;
}
}
function createToast(type: string, message: string): Toast {
const factories = {
success: () => new SuccessToast(message),
error: () => new ErrorToast(message),
warning: () => new WarningToast(message)
};
return factories[type]?.() || new DefaultToast(message);
}📚 Common Front-End Examples
1. Modal Component Factory
import React from 'react';
interface Modal {
open(): void;
close(): void;
render(): React.ReactNode;
}
class ConfirmationModal implements Modal {
constructor(
private title: string,
private onConfirm: () => void
) {}
open() {
// Open logic
}
close() {
// Close logic
}
render() {
return (
<Dialog>
<Dialog.Title>{this.title}</Dialog.Title>
<Dialog.Actions>
<Button onClick={this.onConfirm}>Confirm</Button>
<Button onClick={this.close}>Cancel</Button>
</Dialog.Actions>
</Dialog>
);
}
}
class FormModal<T extends Record<string, unknown>> implements Modal {
constructor(
private form: React.ComponentType<{ onSubmit: (data: T) => void }>,
private onSubmit: (data: T) => void
) {}
render() {
return (
<Dialog>
<this.form onSubmit={this.onSubmit} />
</Dialog>
);
}
}
interface ConfirmationModalConfig {
title: string;
onConfirm: () => void;
}
interface FormModalConfig<T extends Record<string, unknown>> {
form: React.ComponentType<{ onSubmit: (data: T) => void }>;
onSubmit: (data: T) => void;
}
function createModal<T extends Record<string, unknown>>(
type: 'confirmation',
config: ConfirmationModalConfig
): Modal;
function createModal<T extends Record<string, unknown>>(
type: 'form',
config: FormModalConfig<T>
): Modal;
function createModal<T extends Record<string, unknown>>(
type: 'confirmation' | 'form',
config: ConfirmationModalConfig | FormModalConfig<T>
): Modal {
switch (type) {
case 'confirmation':
return new ConfirmationModal(config.title, config.onConfirm);
case 'form':
return new FormModal(config.form, config.onSubmit);
default:
throw new Error(`Unknown modal type: ${type}`);
}
}
// Usage
function App() {
const handleDelete = () => {
const deleteItem = () => {
console.log('Item deleted');
};
const modal = createModal('confirmation', {
title: 'Are you sure?',
onConfirm: deleteItem
});
modal.open();
};
return <button onClick={handleDelete}>Delete</button>;
}2. API Service Factory
interface ApiService {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: unknown): Promise<T>;
put<T>(url: string, data: unknown): Promise<T>;
delete<T>(url: string): Promise<T>;
}
class RestApiService implements ApiService {
constructor(private baseURL: string) {}
async get<T>(url: string): Promise<T> {
const res = await fetch(`${this.baseURL}${url}`);
return res.json() as Promise<T>;
}
async post<T>(url: string, data: unknown): Promise<T> {
const res = await fetch(`${this.baseURL}${url}`, {
method: 'POST',
body: JSON.stringify(data)
});
return res.json();
}
// ... other methods
}
class GraphQLApiService implements ApiService {
constructor(private endpoint: string) {}
async get(url: string) {
// GraphQL doesn't use traditional GET
return this.query(url);
}
async query(query: string) {
const res = await fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify({ query })
});
return res.json();
}
// ... GraphQL implementation
}
interface RestApiConfig {
baseURL: string;
}
interface GraphQLApiConfig {
endpoint: string;
}
function createApiService(type: 'rest', config: RestApiConfig): ApiService;
function createApiService(type: 'graphql', config: GraphQLApiConfig): ApiService;
function createApiService(type: 'rest' | 'graphql', config: RestApiConfig | GraphQLApiConfig): ApiService {
switch (type) {
case 'rest':
return new RestApiService(config.baseURL);
case 'graphql':
return new GraphQLApiService(config.endpoint);
default:
throw new Error(`Unknown API type: ${type}`);
}
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
async function fetchUsers() {
const api = createApiService('rest', { baseURL: 'https://api.example.com' });
const users = await api.get<User[]>('/users');
return users;
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}3. Form Validator Factory
interface Validator {
validate(value: unknown): boolean;
getMessage(): string;
}
class EmailValidator implements Validator {
validate(value: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
getMessage() {
return 'Invalid email';
}
}
class MinLengthValidator implements Validator {
constructor(private minLength: number) {}
validate(value: string) {
return value.length >= this.minLength;
}
getMessage() {
return `Minimum ${this.minLength} characters`;
}
}
class RequiredValidator implements Validator {
validate(value: unknown) {
return value !== null && value !== undefined && value !== '';
}
getMessage() {
return 'Field is required';
}
}
interface MinLengthConfig {
minLength: number;
}
function createValidator(type: 'email'): Validator;
function createValidator(type: 'required'): Validator;
function createValidator(type: 'minLength', config: MinLengthConfig): Validator;
function createValidator(
type: 'email' | 'required' | 'minLength',
config?: MinLengthConfig
): Validator {
switch (type) {
case 'email':
return new EmailValidator();
case 'required':
return new RequiredValidator();
case 'minLength':
return new MinLengthValidator(config.minLength);
default:
throw new Error(`Unknown validator type: ${type}`);
}
}
// Usage
function useFormField(name: string, validators: Validator[]) {
const [value, setValue] = useState('');
const errors = validators
.filter(v => !v.validate(value))
.map(v => v.getMessage());
return { value, setValue, errors, isValid: errors.length === 0 };
}
const emailField = useFormField('email', [
createValidator('required'),
createValidator('email')
]);4. Notification Component Factory
import { useState } from 'react';
interface Notification {
id: string;
type: string;
message: string;
duration?: number;
render(): React.ReactNode;
}
class SuccessNotification implements Notification {
id = Math.random().toString();
type = 'success';
constructor(
public message: string,
public duration: number = 3000
) {}
render() {
return (
<div className="notification success">
✓ {this.message}
</div>
);
}
}
class ErrorNotification implements Notification {
id = Math.random().toString();
type = 'error';
constructor(
public message: string,
public duration: number = 5000
) {}
render() {
return (
<div className="notification error">
✗ {this.message}
</div>
);
}
}
class WarningNotification implements Notification {
id = Math.random().toString();
type = 'warning';
constructor(
public message: string,
public duration: number = 4000
) {}
render() {
return (
<div className="notification warning">
⚠ {this.message}
</div>
);
}
}
class InfoNotification implements Notification {
id = Math.random().toString();
type = 'info';
constructor(
public message: string,
public duration: number = 3000
) {}
render() {
return (
<div className="notification info">
ℹ {this.message}
</div>
);
}
}
function createNotification(
type: 'success' | 'error' | 'warning' | 'info',
message: string,
duration?: number
): Notification {
const factories = {
success: () => new SuccessNotification(message, duration),
error: () => new ErrorNotification(message, duration),
warning: () => new WarningNotification(message, duration),
info: () => new InfoNotification(message, duration)
};
return factories[type]();
}
// Usage
function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const show = (type: 'success' | 'error' | 'warning' | 'info', message: string) => {
const notification = createNotification(type, message);
setNotifications(prev => [...prev, notification]);
setTimeout(() => {
setNotifications(prev => prev.filter(n => n.id !== notification.id));
}, notification.duration);
};
return { notifications, show };
}
function NotificationContainer() {
const { notifications, show } = useNotifications();
return (
<div>
<button onClick={() => show('success', 'Operation completed!')}>
Show Success
</button>
<button onClick={() => show('error', 'Something went wrong')}>
Show Error
</button>
<div>
{notifications.map(notification => (
<div key={notification.id}>{notification.render()}</div>
))}
</div>
</div>
);
}5. Event Handler Factory
import { useEffect } from 'react';
interface EventHandler {
handle(event: Event): void;
}
class ClickHandler implements EventHandler {
constructor(private callback: () => void) {}
handle(event: MouseEvent) {
this.callback();
}
}
class KeyboardHandler implements EventHandler {
constructor(
private key: string,
private callback: () => void
) {}
handle(event: KeyboardEvent) {
if (event.key === this.key) {
this.callback();
}
}
}
class ScrollHandler implements EventHandler {
constructor(
private threshold: number,
private callback: () => void
) {}
handle(event: Event) {
const scrollY = window.scrollY;
if (scrollY > this.threshold) {
this.callback();
}
}
}
interface ClickHandlerConfig {
callback: () => void;
}
interface KeyboardHandlerConfig {
key: string;
callback: () => void;
}
interface ScrollHandlerConfig {
threshold: number;
callback: () => void;
}
type EventHandlerConfig = ClickHandlerConfig | KeyboardHandlerConfig | ScrollHandlerConfig;
function createEventHandler(
type: 'click',
config: ClickHandlerConfig
): EventHandler;
function createEventHandler(
type: 'keyboard',
config: KeyboardHandlerConfig
): EventHandler;
function createEventHandler(
type: 'scroll',
config: ScrollHandlerConfig
): EventHandler;
function createEventHandler(
type: 'click' | 'keyboard' | 'scroll',
config: EventHandlerConfig
): EventHandler {
switch (type) {
case 'click':
return new ClickHandler(config.callback);
case 'keyboard':
return new KeyboardHandler(config.key, config.callback);
case 'scroll':
return new ScrollHandler(config.threshold, config.callback);
default:
throw new Error(`Unknown event type: ${type}`);
}
}
// Usage
function useEventHandlers(handlers: EventHandler[]) {
useEffect(() => {
handlers.forEach(handler => {
window.addEventListener('click', handler.handle);
});
return () => {
handlers.forEach(handler => {
window.removeEventListener('click', handler.handle);
});
};
}, [handlers]);
}
function App() {
const handlers = [
createEventHandler('click', {
callback: () => console.log('Clicked!')
}),
createEventHandler('keyboard', {
key: 'Escape',
callback: () => console.log('Escape pressed!')
}),
createEventHandler('scroll', {
threshold: 100,
callback: () => console.log('Scrolled past 100px!')
})
];
useEventHandlers(handlers);
return <div>App content</div>;
}🎯 When to Use
This pattern is commonly used and recommended for:
- Creating modals/toasts - Different notification types with the same interface
- API services - Switch between REST, GraphQL, gRPC without changing client code
- Validation systems - Create validators dynamically based on configuration
- UI components - Create component variants (Button, Card, etc.)
- Event handlers - Create different event handler types with the same interface
📚 Key Takeaways
- Encapsulates creation - Centralizes object creation logic
- Flexibility - Swap implementations without changing client code
- Extensibility - Add new types without modifying existing code
- Separation of concerns - Client doesn't need to know how to create objects
- Testability - Easy to mock factories in tests