Front-end Engineering Lab
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:

  1. Form validation - Different validation rules for different fields
  2. Cache systems - Switch between memory, localStorage, IndexedDB without changing code
  3. Rendering strategies - Alternate between SSR, SSG, CSR based on route
  4. Payment systems - Different gateways (Stripe, PayPal, etc.)
  5. 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

On this page