DEV Community

artydev
artydev

Posted on

Vanilla JS Signal implementation

This is a pure signal implementation in Javascript, an improved version by ChatGPT and Blackbox from the code provided in :

Poor mans signal

export class Signal extends EventTarget {
    #value;
    #listeners = new Set();
    #isNotifying = false; // Flag to track if we are currently notifying listeners

    get value() {
        return this.#value;
    }

    set value(value) {
        if (this.#value === value) return;
        this.#value = value;
        this.#notify();
    }

    constructor(value) {
        super();
        this.#value = value;
    }

    /**
     * Registers an effect function that will be called when the signal changes.
     * @param {Function} fn - The effect function to run on change.
     * @returns {Function} A cleanup function to unregister the effect.
     */
    effect(fn) {
        const wrappedFn = () => {
            try {
                fn();
            } catch (error) {
                console.error("Effect error:", error);
            }
        };

        wrappedFn(); // Run the effect once immediately
        this.#listeners.add(wrappedFn);
        this.addEventListener("change", wrappedFn);

        return () => {
            this.#listeners.delete(wrappedFn);
            this.removeEventListener("change", wrappedFn);
        };
    }

    #notify() {
        if (this.#listeners.size > 0 && !this.#isNotifying) {
            this.#isNotifying = true; // Set the flag to prevent re-entrance
            queueMicrotask(() => {
                this.dispatchEvent(new CustomEvent("change"));
                this.#isNotifying = false; // Reset the flag after notifying
            });
        }
    }

    valueOf() {
        return this.#value;
    }

    toString() {
        return String(this.#value);
    }
}

export class Computed extends Signal {
    #fn;
    #deps;

    constructor(fn, deps) {
        super(Computed.#computeInitialValue(fn, deps));
        this.#fn = fn;
        this.#deps = deps;

        for (const dep of deps) {
            if (dep instanceof Signal) {
                dep.effect(() => this.#update());
            } else {
                console.warn("Computed dependency is not a Signal:", dep);
                throw new TypeError("All dependencies must be instances of Signal.");
            }
        }
    }

    static #computeInitialValue(fn, deps) {
        try {
            return fn(...deps.map(dep => dep.value));
        } catch (error) {
            console.error("Error computing initial value of Computed:", error);
            return undefined;
        }
    }

    #update() {
        try {
            const newValue = this.#fn(...this.#deps.map(dep => dep.value));
            if (this.value !== newValue) {
                super.value = newValue; // Update using Signal's setter
            }
        } catch (error) {
            console.error("Error updating Computed value:", error);
        }
    }
}

/**
 * Creates a new Signal instance with the given initial value.
 * @param {*} initialValue - The initial value of the signal.
 * @returns {Signal} A new Signal instance.
 */
export const signal = (initialValue) => new Signal(initialValue);

/**
 * Creates a new Computed instance that derives its value from the given function and dependencies.
 * @param {Function} fn - The function to compute the value.
 * @param {Signal[]} deps - An array of Signal instances that the computed value depends on.
 * @returns {Computed} A new Computed instance.
 */
export const computed = (fn, deps) => new Computed(fn, deps);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)