Design Patterns
Common design patterns for frontend development
Design Patterns
Design patterns provide proven solutions to common problems. This covers patterns frequently used in frontend development.
Module Pattern
ES Modules
// math.ts - clean exports
export const PI = 3.14159;
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// Default export for main functionality
export default class Calculator {
// ...
}
// usage.ts
import Calculator, { add, multiply, PI } from './math';
import * as math from './math';Revealing Module Pattern
// Private implementation, public interface
function createCounter() {
// Private
let count = 0;
const MAX_COUNT = 100;
function validateCount(n: number): boolean {
return n >= 0 && n <= MAX_COUNT;
}
// Public API
return {
getCount: () => count,
increment: () => {
if (validateCount(count + 1)) {
count++;
}
return count;
},
decrement: () => {
if (validateCount(count - 1)) {
count--;
}
return count;
},
reset: () => {
count = 0;
return count;
},
};
}
const counter = createCounter();
counter.increment(); // 1
counter.getCount(); // 1Singleton Pattern
// Class-based singleton
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
this.logs.push(`[${new Date().toISOString()}] ${message}`);
console.log(message);
}
getLogs(): string[] {
return [...this.logs];
}
}
// Usage
const logger = Logger.getInstance();
logger.log('Application started');
// Module singleton (simpler)
// logger.ts
class LoggerService {
private logs: string[] = [];
log(message: string): void {
this.logs.push(message);
}
}
export const logger = new LoggerService();Factory Pattern
// Component factory
interface NotificationProps {
message: string;
type: 'success' | 'error' | 'warning' | 'info';
}
function createNotification({ message, type }: NotificationProps) {
const config = {
success: { icon: CheckIcon, color: 'green' },
error: { icon: XIcon, color: 'red' },
warning: { icon: AlertIcon, color: 'yellow' },
info: { icon: InfoIcon, color: 'blue' },
};
const { icon: Icon, color } = config[type];
return (
<div className={`notification notification-${color}`}>
<Icon />
<span>{message}</span>
</div>
);
}
// API client factory
interface ApiClientConfig {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
function createApiClient(config: ApiClientConfig) {
const { baseUrl, timeout = 5000, headers = {} } = config;
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const response = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers: { ...headers, ...options.headers },
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
return {
get: <T>(endpoint: string) => request<T>(endpoint),
post: <T>(endpoint: string, data: unknown) =>
request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}),
put: <T>(endpoint: string, data: unknown) =>
request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}),
delete: <T>(endpoint: string) =>
request<T>(endpoint, { method: 'DELETE' }),
};
}
// Usage
const api = createApiClient({ baseUrl: 'https://api.example.com' });
const users = await api.get<User[]>('/users');Observer Pattern
// Event emitter
type Listener<T> = (data: T) => void;
class EventEmitter<Events extends Record<string, any>> {
private listeners = new Map<keyof Events, Set<Listener<any>>>();
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
// Return unsubscribe function
return () => this.off(event, listener);
}
off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
this.listeners.get(event)?.delete(listener);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}
// Type-safe events
interface AppEvents {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string };
notification: { message: string; type: 'success' | 'error' };
}
const events = new EventEmitter<AppEvents>();
// Subscribe
const unsubscribe = events.on('userLogin', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
// Emit
events.emit('userLogin', { userId: '123', timestamp: new Date() });
// Unsubscribe
unsubscribe();React Hook for Events
function useEvent<T>(
emitter: EventEmitter<any>,
event: string,
handler: (data: T) => void
) {
useEffect(() => {
return emitter.on(event, handler);
}, [emitter, event, handler]);
}
// Usage
function NotificationListener() {
useEvent(events, 'notification', ({ message, type }) => {
showToast(message, type);
});
return null;
}Strategy Pattern
// Validation strategies
interface ValidationStrategy {
validate(value: string): { valid: boolean; message?: string };
}
const emailStrategy: ValidationStrategy = {
validate(value) {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
return { valid, message: valid ? undefined : 'Invalid email format' };
},
};
const passwordStrategy: ValidationStrategy = {
validate(value) {
const valid = value.length >= 8 && /[A-Z]/.test(value) && /\d/.test(value);
return {
valid,
message: valid ? undefined : 'Password must be 8+ chars with uppercase and number',
};
},
};
const phoneStrategy: ValidationStrategy = {
validate(value) {
const valid = /^\+?[\d\s-]{10,}$/.test(value);
return { valid, message: valid ? undefined : 'Invalid phone number' };
},
};
// Validator using strategies
class FormValidator {
private strategies = new Map<string, ValidationStrategy>();
addStrategy(field: string, strategy: ValidationStrategy) {
this.strategies.set(field, strategy);
}
validate(field: string, value: string) {
const strategy = this.strategies.get(field);
if (!strategy) return { valid: true };
return strategy.validate(value);
}
validateAll(data: Record<string, string>) {
const errors: Record<string, string> = {};
let isValid = true;
for (const [field, value] of Object.entries(data)) {
const result = this.validate(field, value);
if (!result.valid) {
isValid = false;
errors[field] = result.message!;
}
}
return { isValid, errors };
}
}
// Usage
const validator = new FormValidator();
validator.addStrategy('email', emailStrategy);
validator.addStrategy('password', passwordStrategy);
const result = validator.validateAll({
email: 'test@example.com',
password: 'weak',
});
// { isValid: false, errors: { password: '...' } }Decorator Pattern
// Function decorator
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`${fn.name} returned`, result);
return result;
}) as T;
}
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2); // Logs: Calling add with [1, 2], add returned 3
// HOC as decorator (React)
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function AuthenticatedComponent(props: P) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <LoadingSpinner />;
if (!isAuthenticated) return <Navigate to="/login" />;
return <WrappedComponent {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);
// TypeScript decorators (experimental)
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
class UserService {
@log
async getUser(id: string) {
// ...
}
}Proxy Pattern
// Reactive proxy (like Vue 3)
function reactive<T extends object>(target: T): T {
const handlers: ProxyHandler<T> = {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
track(target, prop); // Track dependency
return typeof value === 'object' ? reactive(value) : value;
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
trigger(target, prop); // Trigger updates
return result;
},
};
return new Proxy(target, handlers);
}
// Lazy loading proxy
function createLazyProxy<T extends object>(
loader: () => Promise<T>
): T {
let instance: T | null = null;
let loading: Promise<T> | null = null;
return new Proxy({} as T, {
get(_, prop) {
if (!instance) {
if (!loading) {
loading = loader().then(result => {
instance = result;
return result;
});
}
throw loading; // For Suspense
}
return instance[prop as keyof T];
},
});
}Mediator Pattern
// Central mediator for component communication
class ComponentMediator {
private components = new Map<string, any>();
register(name: string, component: any) {
this.components.set(name, component);
}
unregister(name: string) {
this.components.delete(name);
}
send(from: string, to: string, message: any) {
const target = this.components.get(to);
if (target?.receive) {
target.receive(from, message);
}
}
broadcast(from: string, message: any) {
this.components.forEach((component, name) => {
if (name !== from && component.receive) {
component.receive(from, message);
}
});
}
}
// Usage with React context
const MediatorContext = createContext<ComponentMediator | null>(null);
function useMediator(name: string, handler: (from: string, msg: any) => void) {
const mediator = useContext(MediatorContext);
useEffect(() => {
const component = { receive: handler };
mediator?.register(name, component);
return () => mediator?.unregister(name);
}, [mediator, name, handler]);
return {
send: (to: string, message: any) => mediator?.send(name, to, message),
broadcast: (message: any) => mediator?.broadcast(name, message),
};
}Best Practices
Design Pattern Guidelines
- Don't force patterns - use when they solve a real problem
- Prefer composition over inheritance
- Keep patterns simple and maintainable
- Document pattern usage for team understanding
- Consider React/Vue built-in patterns first
- Test pattern implementations thoroughly
- Refactor to patterns when complexity warrants it