Skip to content

Instantly share code, notes, and snippets.

@nichoth
Forked from KonnorRogers/signals.html
Created August 13, 2025 21:57
Show Gist options
  • Save nichoth/c168e52b4872e76d94e4b3cb09318c60 to your computer and use it in GitHub Desktop.
Save nichoth/c168e52b4872e76d94e4b3cb09318c60 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment