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

  1. Creating modals/toasts - Different notification types with the same interface
  2. API services - Switch between REST, GraphQL, gRPC without changing client code
  3. Validation systems - Create validators dynamically based on configuration
  4. UI components - Create component variants (Button, Card, etc.)
  5. 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

On this page