Coroutines in JavaScript for web components
lorenzofox.dev
lorenzofox.dev
function Counter(buttonElement) {
this.clicks = 0;
this.handleEvent = function(event) {
switch (event.type) {
case 'click':
this.clicks++;
console.log("I was clicked " + this.clicks + " times." );
break;
//handle any other event types
}
};
buttonElement.addEventListener("click", this, false);
}
const myCounter = new Counter( someButtonElement );
(In case you wouldn't know: if an object is used as an event listener, its `handleEvent()` method will be called with that object bound to `this`. In a way, arrow functions are working around what had been already solved by this mechanism.)In addition, with the `handleEvent` approach, you need to handle all events within a single function. But it's fairly easy to create multiple functions within a single function scope and pass them to different event handlers, thus avoiding the need for the large (and potentially error-prone) switch statement if you end up needing to handle lots of events.
Have you found cases where `handleEvent` works better than just defining local variables within a function and just using those? It seems to me that you wouldn't even need arrow functions to take advantage of the natural power of closures in this context.
Regarding prototypes, mind how this shares methods between instances, rather than consuming (and locking) resources by individual closures created in each of the instances (which, when GC was still based on reference count, would also have meant memory leaks):
function Counter(buttonElement) {
this.clicked = 0;
buttonElement.addEventlistener("click", this, false);
}
Counter.prototype = {
reset: function() { this.clicked = 0; },
log: function() { concole.log("I was clicked " + this.clicked + " times."); },
handleEvent: function(event) { this.clicked++; this.log(); }
};> Crank uses generator functions to define stateful components. You store state in local variables, and `yield` rather than `return` to keep it around.
The most significant unknown for me is how state management (like Redux etc.) would work in a larger app. I can think of many solutions but I can't figure out whether the tradeoffs then result in a net gain or loss.
The author shows a plausible way to put these primitives together to create coroutines powering rendering and lifecycles for web components. And although I'm pretty familiar with the syntax involved, I still learned some new ways to put it together from this article.
Also the author's previous intro article to coroutines in JavaScript might be helpful for learning the basic syntax and patterns: https://lorenzofox.dev/posts/coroutine/
I honestly can't think of an example that couldn't be handled in a stateful way, by using objects/class instances instead of closures.
Even iteration can be handled by implementing [Symbol.iterator] on the instance.
The one place I found a use for them is to syncify async functions in some rare cases where it's needed, as the API would only work with other non-sync functions.
const foo = async function() {
const user = await fetchUser()
But if they'd just made a Promise.coroutine, you could equivalently do this without needing to change the language: const foo = co(function* () {
const user = yield fetchUser()
There's some unpopular cases like async iterables or async generators, but for the most part we could've done the same thing without extending the language. I remember the v1 of koa that had yield everywhere and people thought it was confusing af. Then they released with await and suddenly it made sense to people.There is a lot of very nice functionality in Node around it - pipe functions and the like, and it can work, but in the end I’ve settled on just using async iterators.
It’s just so easy and understandable to go
for await (const item of my AsyncIterator) { … }
And for people who don’t grasp in detail how it all works, the abstraction is very easy to use.I wrote my own ui/effects barebones runtime on it a few years back https://github.com/marcellerusu/capable-js
It’s especially cool when you have components that have distinct states they could be in, eg. a survey
Is this a good thing?
> while(true)
You're a braver man than I. function * makeSequence() {
let i = 1;
while (true) yield i++;
}
This function returns an iterator: const ints = makeSequence();
And now the caller is control of iteration, not the callee: function sumTo(cutoff) {
const ints = makeSequence();
let sum = 0;
for (let i = 0; i < cutoff; i++) {
sum += ints.next().value;
}
return sum;
}
In this case there is no risk of an infinite loop.I realize that, and it's why I (and most) have avoided the syntax entirely. Generators today feel more like a vestigial appendix to the language; an aborted branch of development, than anything I'd ever use or suggest.
while (0.1 + 0.2 !== 0.3)It's a bit of a meme that takes the idea of "React Server Components" to their logical extreme. And coding a PoC is beyond my capabilities... but I'd love to see someone do it.