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)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay