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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more