Skip to content

Instantly share code, notes, and snippets.

@KonnorRogers
Created August 6, 2025 21:41
Show Gist options
  • Save KonnorRogers/5ed37d8227b28e26f91c902abf6ab181 to your computer and use it in GitHub Desktop.
Save KonnorRogers/5ed37d8227b28e26f91c902abf6ab181 to your computer and use it in GitHub Desktop.
Lite Signals
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Signals</title>
</head>
<body>
<button id="decrement">
-
</button>
<span id="counter">0</span>
<button id="increment">
+
</button>
<br><br>
<button id="reset">
Reset
</button>
<script type="module">
// Signals implementation:
class Effect {
constructor () {
this.effects = []
/**
* We initialize an `updateComplete` property with a new
* Promise that we'll only resolve once `this.flushEffects()` has been called
*/
this.updateComplete = this.__createDeferredPromise();
}
add (callback) {
this.effects.push(callback)
}
// https://medium.com/ing-blog/litelement-a-deepdive-into-batched-updates-b9431509fc4f
async requestUpdate () {
if (!this.updateRequested) {
this.updateRequested = true;
this.updateRequested = await false;
this.flushEffects(); // if we move to async versions, make sure to `await` here.
this.__resolve();
this.updateComplete = this.__createDeferredPromise();
}
}
__createDeferredPromise() {
return new Promise((resolve) => {
this.__resolve = resolve;
});
}
flushEffects () {
// We could do this, but order is not guaranteed for resolving.
// return Promise.allSettled(this.effects.map(async (callback) => await callback()))
// We could do this to resolve all effects in order, but its much slower:
// for (const effect of this.effects) { await effect() }
// Or we can just fire it all off synchronously.
for (const effect of this.effects) {
effect()
}
}
}
class Signal {
// Setup a "singleton" to be shared by all instances of "Signal".
static effect = new Effect()
constructor (val) {
this._val = val
}
get value () {
return this._val
}
requestUpdate () {
this.constructor.effect.requestUpdate()
}
set value(val) {
if (this._val === val) return;
this._val = val
this.requestUpdate()
}
/*
* Allows unwrapping when used with operators
* @example
* const signal = new Signal(0)
* console.log(+signal) // => 0
*/
valueOf () {
return this.value
}
}
// Usage:
const counter = document.querySelector("#counter")
const count = new Signal(Number(counter.innerText))
const effect = Signal.effect
// This runs after signals have updated.
effect.add(() => {
counter.innerText = count.value
})
function increment () {
count.value++ // count.value += 1
}
function decrement () {
count.value-- // count.value -= 1
}
function reset () {
count.value = 0
}
document.querySelector("#increment").addEventListener("click", increment)
document.querySelector("#decrement").addEventListener("click", decrement)
document.querySelector("#reset").addEventListener("click", reset)
</script>
</body>
</html>
@KonnorRogers
Copy link
Author

This is essentially the implementation of signals:

      // Signals implementation:
      class Effect {
        constructor () {
          this.effects = []

          /**
          * We initialize an `updateComplete` property with a new
          * Promise that we'll only resolve once `this.flushEffects()` has been called
          */
          this.updateComplete = this.__createDeferredPromise();
        }

        add (callback) {
          this.effects.push(callback)
        }

        // https://medium.com/ing-blog/litelement-a-deepdive-into-batched-updates-b9431509fc4f
        async requestUpdate () {
          if (!this.updateRequested) {
            this.updateRequested = true;
            this.updateRequested = await false;
            this.flushEffects(); // if we move to async versions, make sure to `await` here.
            this.__resolve();
            this.updateComplete = this.__createDeferredPromise();
          }
        }

        __createDeferredPromise() {
          return new Promise((resolve) => {
            this.__resolve = resolve;
          });
        }

        flushEffects () {
          // We could do this, but order is not guaranteed for resolving.
          // return Promise.allSettled(this.effects.map(async (callback) => await callback()))

          // We could do this to resolve all effects in order, but its much slower:
          // for (const effect of this.effects) { await effect() }

          // Or we can just fire it all off synchronously.
          for (const effect of this.effects) {
            effect()
          }
        }
      }

      class Signal {
        // Setup a "singleton" to be shared by all instances of "Signal".
        static effect = new Effect()

        constructor (val) {
          this._val = val
        }

        get value () {
          return this._val
        }

        requestUpdate () {
          this.constructor.effect.requestUpdate()
        }

        set value(val) {
          if (this._val === val) return;

          this._val = val
          this.requestUpdate()
        }

        /*
        * Allows unwrapping when used with operators
        * @example
        *   const signal = new Signal(0)
        *   console.log(+signal) // => 0
        */
        valueOf () {
          return this.value
        }
      }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment