Reactivity ‐ Proxy vs Signal - kdaisho/Blog GitHub Wiki

1. Proxy pattern

function reactive(target) {
  const listeners = new Set()

  function subscribe(listener) {
    listeners.add(listener)
  }

  const proxy = new Proxy(target, {
    get(obj, prop) {
      return obj[prop]
    },
    set(obj, prop, value) {
      obj[prop] = value
      listeners.forEach(listener => listener()) // notify all listeners
      return true
    },
  })

  return { proxy, subscribe }
}

// usage example
const state = reactive({ count: 0 })

// subscribe to changes
state.subscribe(() => {
  console.log("state changed to:", state.proxy.count)
})

// updating the value
state.proxy.count = 1 // output: state change to: 1
state.proxy.count = 20 // output: state change to: 20

2. Signal pattern

function signal(initialValue) {
  let value = initialValue
  const listeners = new Set()

  function get() {
    return value
  }

  function set(newValue) {
    value = newValue
    listeners.forEach((listener) => listener())
  }

  function subscribe(listener) {
    listeners.add(listener)

    return () => listeners.delete(listener) // unsubscribe function
  }

  return { get, set, subscribe }
}

// usage example
const count = signal(0)

// subscribe to changes
count.subscribe(() => {
  console.log("count updated to:", count.get())
})

// update the value
count.set(1) // output: count updated to: 1
count.set(200) // output: count updated to: 200

Key takeaways

1. Conceptual basis

  • Proxy pattern: Uses JavaScript's Proxy objects to intercept operations on objects. It allows for tracking changes to properties through get and set traps. This pattern is powerful for creating reactive objects but can introduce complexity due to the need for property interception.
  • Signal pattern: Focuses on creating reactive values (or "signals") that notify listeners when they change. This approach promotes a clear separation of concerns, making it easier to manage state and updates without intercepting every property access.

2. Performance

  • Proxy pattern: Can incur performance overhead due to the interception of every property access and modification. This might lead to slower updates, especially with deeply nested properties or large objects.
  • Signal pattern: Generally provides better performance for reactive updates. Since signals manage state changes more directly and only notify listeners of relevant changes, then can be more efficient, particularly in scenarios with frequent updates.

Signal pattern in TypeScript example

type Listener<T> = (newValue: T) => void;

class Signal<T> {
  private _value: T;
  private listeners: Set<Listener<T>> = new Set();

  constructor(initialValue: T) {
    this._value = initialValue;
  }

  get value(): T {
    return this._value;
  }

  set value(newValue: T) {
    if (newValue !== this._value) {
      this._value = newValue;
      this.notify();
    }
  }

  subscribe(listener: Listener<T>): () => void {
    this.listeners.add(listener);
    // return an unsubscribe function
    return () => this.listeners.delete(listener);
  }

  private notify(): void {
    this.listeners.forEach(listener => listener(this._value));
  }
}

// usage examples
const counterSignal = new Signal(0);
const themeSignal = new Signal("light");

// subscribing to counter changes
counterSignal.subscribe(newValue => {
  console.log("counter updated:", newValue);
});

// subscribing to theme changes
themeSignal.subscribe(newTheme => {
  console.log("theme updated:", newTheme);
});

// trigger updates
counterSignal.value = 23 // output: counter updated: 23
themeSignal.value = "dark" // output: theme updated: "dark"
⚠️ **GitHub.com Fallback** ⚠️