TechLead
πŸ—οΈ
Advanced
6 min read

Design Patterns

Common design patterns in frontend development

Design patterns are reusable solutions to common software design problems. In frontend interviews, you are expected to recognise patterns in existing code and explain how to apply them. More importantly, you should know why a pattern solves a specific problem β€” not just what it is called.

Creational Patterns

Singleton

Ensures a class has only one instance and provides global access. In JavaScript, module singletons are idiomatic.

// Module-level singleton β€” the module system caches the export
let instance;
class Store {
  constructor() { if (instance) return instance; instance = this; this.state = {}; }
}
export const store = new Store(); // same object every import

Factory

Creates objects without specifying the exact class. Useful when the type of object depends on input.

function createNotification(type, message) {
  const base = { message, timestamp: Date.now() };
  const types = {
    success: () => ({ ...base, icon: 'βœ“', color: 'green' }),
    error:   () => ({ ...base, icon: 'βœ—', color: 'red' }),
    info:    () => ({ ...base, icon: 'β„Ή', color: 'blue' }),
  };
  return (types[type] || types.info)();
}

Structural Patterns

Observer / Pub-Sub

One-to-many dependency: when a subject changes state, all its observers are notified. This is the pattern behind DOM events, React's useState, and most state management libraries.

class EventEmitter {
  #listeners = new Map();
  on(event, cb) {
    if (!this.#listeners.has(event)) this.#listeners.set(event, []);
    this.#listeners.get(event).push(cb);
    return () => this.off(event, cb); // return unsubscribe function
  }
  emit(event, data) { (this.#listeners.get(event) || []).forEach(cb => cb(data)); }
  off(event, cb) { this.#listeners.set(event, (this.#listeners.get(event) || []).filter(l => l !== cb)); }
}

Decorator

Wraps an object to add behaviour without modifying the original. Higher-order components and higher-order functions are decorator implementations.

// Higher-order function decorator β€” adds logging
function withLogging(fn) {
  return function(...args) {
    console.log('Calling', fn.name, 'with', args);
    const result = fn.apply(this, args);
    console.log('Result:', result);
    return result;
  };
}

const loggedFetch = withLogging(fetch);

Behavioural Patterns

Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Eliminates large if/else chains.

const sortStrategies = {
  byName:  (a, b) => a.name.localeCompare(b.name),
  byDate:  (a, b) => new Date(b.date) - new Date(a.date),
  byPrice: (a, b) => a.price - b.price,
};

function sortItems(items, strategy = 'byName') {
  return [...items].sort(sortStrategies[strategy]);
}

Command

Encapsulates a request as an object, enabling undo/redo and queuing.

class TextEditor {
  #history = [];
  #text = '';
  execute(command) { this.#history.push(command); this.#text = command.execute(this.#text); }
  undo() { const cmd = this.#history.pop(); if (cmd) this.#text = cmd.undo(this.#text); }
}

const insertCommand = (text) => ({
  execute: (current) => current + text,
  undo:    (current) => current.slice(0, -text.length),
});

React-Specific Patterns

  • Compound Components β€” related components share implicit state via Context (e.g. <Select><Option>)
  • Render Props β€” pass a function as a prop to share stateful logic (largely superseded by hooks)
  • Custom Hooks β€” extract and reuse stateful logic across components
  • Provider Pattern β€” inject dependencies via Context rather than prop-drilling

Continue Learning