PatternsArchitecture Patterns
Strategy Pattern
Swap algorithms or behaviors dynamically without filling code with if/else statements
The Strategy Pattern allows you to define a family of algorithms, encapsulate them, and make them interchangeable. Essential for swapping logic or components dynamically without filling code with if/else statements.
🎯 The Problem
// ❌ BAD: Too many if/else
function calculateShipping(order: Order) {
if (order.shippingType === 'standard') {
return order.weight * 0.5;
} else if (order.shippingType === 'express') {
return order.weight * 1.5;
} else if (order.shippingType === 'overnight') {
return order.weight * 3.0;
}
// More types = more if/else 😱
}
// ✅ GOOD: Strategy Pattern
interface ShippingStrategy {
calculate(weight: number): number;
}
const standardShipping: ShippingStrategy = {
calculate: (weight) => weight * 0.5
};
const expressShipping: ShippingStrategy = {
calculate: (weight) => weight * 1.5
};
function calculateShipping(order: Order, strategy: ShippingStrategy) {
return strategy.calculate(order.weight);
}📚 Common Front-End Examples
1. Form Validation Strategies
import { useState } from 'react';
interface ValidationStrategy {
validate(value: string): boolean;
getMessage(): string;
}
const emailValidation: ValidationStrategy = {
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
getMessage: () => 'Invalid email'
};
const phoneValidation: ValidationStrategy = {
validate: (value) => /^\d{10,11}$/.test(value),
getMessage: () => 'Phone must have 10 or 11 digits'
};
const requiredValidation: ValidationStrategy = {
validate: (value) => value.trim().length > 0,
getMessage: () => 'Field is required'
};
function useFieldValidation(
value: string,
strategies: ValidationStrategy[]
) {
const errors = strategies
.filter(s => !s.validate(value))
.map(s => s.getMessage());
return { isValid: errors.length === 0, errors };
}
// Usage
function EmailInput() {
const [email, setEmail] = useState('');
const { isValid, errors } = useFieldValidation(email, [
requiredValidation,
emailValidation
]);
return (
<div>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
className={isValid ? '' : 'error'}
/>
{errors.map(err => <span key={err}>{err}</span>)}
</div>
);
}2. Rendering Strategies (SSR/CSR/SSG)
import { useState, useEffect } from 'react';
import { renderToString } from 'react-dom/server';
import { writeFile } from 'fs/promises';
interface RenderingStrategy {
render<P extends Record<string, unknown>>(component: React.ComponentType<P>, props: P): Promise<string>;
}
const ssrStrategy: RenderingStrategy = {
render: async (Component, props) => {
// Server-side rendering
return renderToString(<Component {...props} />);
}
};
const ssgStrategy: RenderingStrategy = {
render: async (Component, props) => {
// Pre-render at build time
const html = renderToString(<Component {...props} />);
if ('id' in props && typeof props.id === 'string') {
await writeFile(`./static/${props.id}.html`, html);
}
return html;
}
};
const csrStrategy: RenderingStrategy = {
render: async () => {
// Client-side only
return '<div id="root"></div>';
}
};
function PageRenderer<P extends Record<string, unknown>>({
strategy,
component,
props
}: {
strategy: RenderingStrategy;
component: React.ComponentType<P>;
props: P;
}) {
const [html, setHtml] = useState('');
useEffect(() => {
strategy.render(component, props).then(setHtml);
}, [strategy, component, props]);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// Usage
interface PageProps {
id: string;
title: string;
}
function ProductPage({ id, title }: PageProps) {
return <div><h1>{title}</h1></div>;
}
function App() {
return (
<PageRenderer
strategy={ssrStrategy}
component={ProductPage}
props={{ id: 'product-1', title: 'Product Name' }}
/>
);
}3. Cache Strategies
import { useState, useEffect, useCallback } from 'react';
interface CacheStrategy {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T): Promise<void>;
clear(): Promise<void>;
}
class MemoryCache implements CacheStrategy {
private cache = new Map<string, unknown>();
async get<T>(key: string): Promise<T | null> {
return (this.cache.get(key) as T) || null;
}
async set<T>(key: string, value: T): Promise<void> {
this.cache.set(key, value);
}
async clear(): Promise<void> {
this.cache.clear();
}
}
const localStorageCache: CacheStrategy = {
get: async <T,>(key: string): Promise<T | null> => {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : null;
},
set: async <T,>(key: string, value: T): Promise<void> => {
localStorage.setItem(key, JSON.stringify(value));
},
clear: async (): Promise<void> => {
localStorage.clear();
}
};
// IndexedDB cache (requires idb library)
const indexedDBCache: CacheStrategy = {
get: async <T,>(key: string): Promise<T | null> => {
// Example using idb library
// const db = await openDB('cache', 1);
// return (await db.get('cache', key)) as T | null;
return null; // Placeholder
},
set: async <T,>(key: string, value: T): Promise<void> => {
// const db = await openDB('cache', 1);
// await db.put('cache', value, key);
},
clear: async (): Promise<void> => {
// const db = await openDB('cache', 1);
// await db.clear('cache');
}
};
function useCache<T>(key: string, strategy: CacheStrategy) {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
strategy.get<T>(key).then(setData);
}, [key, strategy]);
const update = useCallback((value: T) => {
strategy.set(key, value).then(() => setData(value));
}, [key, strategy]);
return { data, update };
}
// Usage
function UserProfile() {
const { data: user, update } = useCache<{ id: number; name: string }>(
'user',
new MemoryCache()
);
useEffect(() => {
if (!user) {
update({ id: 1, name: 'John' });
}
}, [user, update]);
return <div>{user?.name}</div>;
}4. Pagination Strategies
import { useState } from 'react';
interface PaginationStrategy {
getPage<T>(items: T[], page: number, pageSize: number): T[];
getTotalPages(totalItems: number, pageSize: number): number;
}
const offsetPagination: PaginationStrategy = {
getPage: <T,>(items: T[], page: number, pageSize: number) => {
const start = (page - 1) * pageSize;
return items.slice(start, start + pageSize);
},
getTotalPages: (total, size) => Math.ceil(total / size)
};
const cursorPagination: PaginationStrategy = {
getPage: <T extends { id: string | number }>(items: T[], cursor: string | number, pageSize: number) => {
const index = items.findIndex(item => item.id === cursor);
return items.slice(index + 1, index + 1 + pageSize);
},
getTotalPages: () => Infinity // Cursor-based doesn't have total pages
};
function usePagination<T>(
items: T[],
strategy: PaginationStrategy,
pageSize: number = 10
) {
const [page, setPage] = useState(1);
const pageData = strategy.getPage(items, page, pageSize);
const totalPages = strategy.getTotalPages(items.length, pageSize);
return {
pageData,
currentPage: page,
totalPages,
nextPage: () => setPage(p => Math.min(p + 1, totalPages)),
prevPage: () => setPage(p => Math.max(p - 1, 1))
};
}
// Usage
interface Product {
id: number;
name: string;
}
function ProductList() {
const products: Product[] = [
{ id: 1, name: 'Product 1' },
{ id: 2, name: 'Product 2' },
{ id: 3, name: 'Product 3' },
// ... more products
];
const { pageData, currentPage, totalPages, nextPage, prevPage } = usePagination(
products,
offsetPagination,
2
);
return (
<div>
{pageData.map(product => (
<div key={product.id}>{product.name}</div>
))}
<button onClick={prevPage} disabled={currentPage === 1}>Previous</button>
<span>Page {currentPage} of {totalPages}</span>
<button onClick={nextPage} disabled={currentPage === totalPages}>Next</button>
</div>
);
}5. Authentication Strategies
import { useState, useCallback } from 'react';
interface LoginCredentials {
email: string;
password: string;
}
interface AuthStrategy {
login(credentials: LoginCredentials): Promise<User>;
logout(): Promise<void>;
refresh(): Promise<string>;
}
const jwtAuth: AuthStrategy = {
login: async (credentials) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const { token, user } = await res.json();
localStorage.setItem('token', token);
return user;
},
logout: async () => {
localStorage.removeItem('token');
},
refresh: async () => {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/refresh', {
headers: { Authorization: `Bearer ${token}` }
});
const { token: newToken } = await res.json();
localStorage.setItem('token', newToken);
return newToken;
}
};
const oauthAuth: AuthStrategy = {
login: async () => {
window.location.href = '/api/auth/oauth';
},
logout: async () => {
await fetch('/api/auth/logout', { method: 'POST' });
},
refresh: async () => {
// OAuth handles refresh automatically
return '';
}
};
interface User {
id: string;
name: string;
email: string;
}
function useAuth(strategy: AuthStrategy) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (credentials: LoginCredentials) => {
const user = await strategy.login(credentials);
setUser(user);
}, [strategy]);
const logout = useCallback(async () => {
await strategy.logout();
setUser(null);
}, [strategy]);
return { user, login, logout };
}
// Usage
function LoginForm() {
const { login, user } = useAuth(jwtAuth);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login({ email, password });
};
if (user) {
return <div>Welcome, {user.name}!</div>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}🎯 When to Use
This pattern is commonly used and recommended for:
- Form validation - Different validation rules for different fields
- Cache systems - Switch between memory, localStorage, IndexedDB without changing code
- Rendering strategies - Alternate between SSR, SSG, CSR based on route
- Payment systems - Different gateways (Stripe, PayPal, etc.)
- Authentication - Support JWT, OAuth, Session-based without conditionals
📚 Key Takeaways
- Eliminates if/else - Replaces conditionals with interchangeable objects
- Open/Closed Principle - Open for extension, closed for modification
- Testability - Each strategy can be tested in isolation
- Flexibility - Swap algorithms at runtime
- Maintainability - Adding new strategy doesn't break existing code