One of the biggest pain points I've experienced with modern web frameworks is the combination of async logic and reactive bindings. Working with async code that only depends indirectly on outer reactive data is manageable, although even that can be cumbersome. But once you try to track reactive dependencies nested inside async calls… you'll probably want to refill your coffee first.
Coffee may help ☕, but I’d rather rely on a more flexible technical approach 🧑💻.
Core Concept
A straightforward solution to track dependencies within async functions is passing an execution context. Typically reactive dependencies are tracked by implicit magic in the background. That's in general great for simplicity but provides limited control and does not work well with async logic. Requiring an explicit context is much less convenient for simple cases, but becomes valuable when things get complex.
Let’s see how that could work in a simple TS example:
class UserCalendar {
readonly userId: ReactiveValue<string> = new ReactiveValue();
readonly date: ReactiveValue<IsoDate> = new ReactiveValue();
protected provider = new CalendarProvider();
async getUserEvents(ec?: ExecutionContext): Promise<Event[]> {
const id = this.userId.get(ec);
const calendar = await this.provider.getUserCalendar(id);
return this.provider.getEvents(calendar, this.date.get(ec));
}
}
The explicit context provides full control over dependency tracking and works well with async execution. It can also define custom triggers to update bindings without workarounds like reactive counters.
I personally prefer to trade some convenience for additional clarity and control, but I understand that always having to pass the context can be considered too tedious. So let's solve that!
Another Layer
To really make this approach shine, we need an additional layer of abstraction. And while we’re at it, wouldn’t it be nice to simplify async execution more fundamentally by defining high-level functionality in a more declarative way?
I think there are many good reasons to use a low-code abstraction layer on top of JS, transparent async logic is one of them. When dealing with high-level features we shouldn’t have to worry whether a method is async or not. Changing from sync to async shouldn’t break anything, it should just work!
Let’s have a look at our previous example again, this time as a low-code service definition:
{
"services": {
"provider": {
"service": "my-project:calendar-provider"
}
},
"methods": {
"getUserEvents": {
"action": {
"_action": "service",
"service": "@provider",
"method": "getEvents",
"params": {
"calendar": {
"_bind": "action",
"read": {
"_action": "service",
"service": "@provider",
"method": "getUserCalendar",
"params": {
"userId": {
"_bind": "data",
"ref": "userId"
}
}
}
},
"date": {
"_bind": "data",
"ref": "date"
}
}
}
}
}
}
This kind of abstraction enables implicit dependency tracking while keeping the advantages of an explicit execution context under the hood.
Of course editing JSON directly would also require a lot of coffee ☕, but I believe with a good visual IDE 🧑💻 it can be a powerful alternative to code-based frameworks for many use cases.
What are your thoughts? Would you consider using this kind of low-code approach for simpler and transparent handling of async execution?
Here you can read more details about my vision for low-code web development:
Top comments (0)