In the previous two articles, we started with what looked like a simple question:
const user = createMemo(async () => {
return fetchUser(id());
});
The first article asked:
Once computed values can be async, why does a reactive runtime stop managing only values and start managing executions?
The second pushed the question further:
When an execution becomes pending, where should the UI actually wait?
Solid 2.0's idea of:
Fetch High, Block Low
shows an important distinction:
Where async work starts ≠ where the UI blocks
Or, put another way:
Execution Boundary ≠ Rendering Boundary
But if we take that idea one step further, we arrive at a more fundamental question:
Does reactive execution have to end in rendering?
This was also one of the main questions behind my original design of signal-kernel and async-runtime.
If the core capabilities of a reactive system are:
dependency tracking
+
change propagation
+
incremental recomputation
then why should they only be useful for updating a UI?
I Never Wanted the Kernel to Own Rendering
When I started designing signal-kernel, I made one decision fairly early:
The kernel itself should not own rendering.
I wanted the reactive graph to exist independently of any UI framework.
The architecture looks roughly like this:
graph TD
A["signal-kernel/core"]
B["Reactive graph"]
C["framework adapter"]
D["React"]
E["Vue"]
A-->B-->C-->D
C-->E
React and Vue are not the owners of the reactive graph.
They are consumers of the graph.
That may sound like little more than a package boundary, but it ends up affecting a lot of later design decisions.
If the runtime does not own rendering, it cannot assume that this sequence always exists:
computation pending
↓
Suspense
↓
UI fallback
The same runtime might be running in:
- a browser
- a worker
- a server
- a CLI
- an agent
Some of those environments may not have a UI at all.
So once async work enters the reactive graph, the runtime should no longer be asking:
What should the screen display?
It needs to ask:
What state is this execution in right now?
Rendering Is Just One Consumer
Once rendering is removed from the reactive runtime itself, the model starts to look more like this:
graph TD
A["Reactive Graph"]
B["Async Runtime"]
C["Execution State"]
D["React"]
E["Vue"]
F["Worker"]
G["Server"]
H["Agent"]
A-->B-->C-->D
C-->E
C-->F
C-->G
C-->H
At that point:
pendingresolvedrejectedstalecancelledsuperseded
are no longer UI states.
They are:
Execution states.
React might translate pending into a loading state.
Vue might choose to represent it differently.
A worker may render nothing at all and simply wait until some other piece of work can continue.
An agent runtime might inspect pending or stale to decide whether a node needs to run again.
The responsibility of the reactive runtime changes accordingly.
It no longer needs to answer:
How should this be rendered?
But it still has to answer questions like:
- Is this execution still valid?
- Who is allowed to cancel it?
- Has a newer execution replaced it?
- Should its result still propagate when it completes?
Async Work Can't Be Reduced to a Promise
Once rendering is no longer the center of the model, another problem becomes much harder to ignore:
Are all forms of async work really the same thing?
At the JavaScript level, they may all return a Promise:
await fetchUser();
await updateUser();
await waitForMessage();
But their semantics are completely different.
The first is closer to a Query. Its main purpose is to obtain a value.
The second is a Mutation. It changes something outside the runtime.
The third, if it represents a long-lived Stream, may not even have a meaningful point where "the Promise has completed" and the work is done.
If we look only at the JavaScript type Promise<T>, it is very easy to collapse all of these into a single abstraction.
From the runtime's point of view, though, they need to answer very different questions.
Query, Mutation, and Stream Have Different Execution Semantics
Consider a Query first:
Query
↓
read external state
↓
produce a value
The runtime may need to ask:
- When does it become
stale? - Should it execute again?
- Can the previous result remain usable temporarily?
A Mutation is different:
Mutation
↓
modify external state
The important question is not only whether it succeeded.
It is:
What did it change?
For example, after updateUser() succeeds, this may mean that:
user
profile
permissions
can no longer be trusted.
This introduces a relationship that dependency tracking alone cannot derive:
Invalidation.
A Stream has yet another execution model:
Stream
↓
first value
↓
second value
↓
third value
↓
...
Its lifecycle is closer to:
start
→ receive
→ receive
→ receive
→ cancel / complete
Now the runtime also has to care about things like:
- session ownership
- stable value
- cancellation
- long-lived lifecycle
So while Query, Mutation, and Stream are all asynchronous work, the runtime should not treat them as identical executions.
Dependency Is Not the Same as Invalidation
Reactive graphs are very good at describing dependencies.
For example:
price
↓
subtotal
↓
total
When price changes, subtotal naturally becomes stale.
That relationship can be discovered by tracking which computations read which values.
Mutation introduces a different kind of relationship.
Suppose:
After
updateProfile()completes,userProfileneeds to be fetched again.
But updateProfile() may never read userProfile while it runs.
That means dependency tracking alone cannot derive:
updateProfile
→ invalidates userProfile
Dependency describes:
Who do I need in order to compute myself?
Invalidation describes:
Whose trustworthiness changes because I happened?
Those are not the same relationship.
You can think of it like this:
graph LR
A["User ID"]
B["Update User Mutation"]
C["User Query"]
D["Profile View"]
A--"depends on"-->C--"depends on"-->D
B--"invalidates"-->C
This is one reason why a plain dependency graph may stop being enough once a reactive runtime begins managing asynchronous work.
The Runtime Starts Needing to Know Whether an Execution Is Still Eligible
When we discussed async computed values earlier, we already ran into this situation:
Execution A
Execution B
If both exist at the same time, which result is allowed to affect the current state?
Once async work becomes a first-class runtime concept, this question appears everywhere.
For example:
revision 1
→ start query A
revision 2
→ start query B
B resolves
→ commit
A resolves later
→ discard
What the runtime is really managing here is:
Whether a particular execution is still eligible to affect the current state.
That requires more than a value.
It also requires concepts such as:
- execution identity
- revision
- cancellation
- supersession
This is why concepts like revisions and cancellation gradually became part of async-runtime.
Once rendering is no longer hiding these details for you, the runtime itself has to define the execution lifecycle clearly.
Cancellation Is Also an Ownership Problem
A normal async function can use an AbortController.
But from the runtime's perspective, the more fundamental question is:
Who has the authority to cancel this execution?
Consider a component being unmounted:
Component
↓
cancel request
This effectively means that the component owns the async lifecycle.
But what if the async work does not belong to the component?
For example:
- an agent runtime
- a worker
- a shared data graph
In that case:
UI disappeared
should not automatically mean:
execution should stop
This is one of the problems that naturally appears once rendering ownership and async ownership are separated.
Render lifecycle ≠ Async lifecycle
Cancellation should be controlled by whoever actually owns the execution.
Streams Make This Difference Even More Obvious
Long-lived streams are a good example.
For instance:
- WebSocket
- SSE
- MQTT
- event subscriptions
Their lifecycle usually looks more like this:
connect
↓
receive
↓
receive
↓
receive
↓
disconnect
If the stream is directly tied to a component lifecycle:
- component mount → connect
- component unmount → disconnect
then the component naturally becomes the stream owner.
But what if the stream is being used for:
- job monitoring
- shared application state
- background processing
- agent events
Its lifetime may be longer than that of any individual UI consumer.
At that point, the UI no longer consuming the stream and the stream session should end cannot mean the same thing.
This is also why ownership became increasingly important to me while designing createStreamResource.
Once Rendering Is No Longer the Endpoint, Snapshot Means Something Different Too
If a runtime exists only to support a UI, a snapshot can easily be interpreted as:
Save the state the current screen needs.
But for an execution runtime, the more interesting question is:
Can we restore the state of the computation itself?
For example:
- Which values are already stable?
- Which executions are still pending?
- Which dependencies are stale?
- What revision are we currently on?
And potentially even:
Can the relationships inside the graph itself be preserved?
At that point, a snapshot is no longer just:
JSON.stringify(state)
It starts to look more like:
runtime state persistence
That can be useful for SSR hydration.
It can help with debugging.
It can also support workflow recovery.
And once you move into agent systems, the value of that capability becomes even more obvious.
Why Are Agents a Natural Next Domain?
An agent workflow looks very different from frontend rendering on the surface.
For example:
Goal
↓
LLM
↓
Tool
↓
Observation
↓
Decision
↓
Retry / Continue / Stop
But if you look at it through the lens of execution semantics, many of the problems are surprisingly familiar to a reactive runtime.
For example:
- context changed → which computations are now stale?
- tool result arrived → which downstream nodes need to be reevaluated?
- a new LLM execution started → is the old execution still valid?
- a correction occurred → which results need to be validated again?
- the workflow stopped → which executions should be cancelled?
None of these are really rendering problems.
They are much closer to:
- dependency
- execution
- invalidation
- ownership
- lifecycle
In other words:
graph TD
A["Goal / Context"]
B["LLM Execution"]
C["Tool Result"]
D["Derived State"]
E["Invalidation / Correction"]
A-->B-->C-->D
E-->D
D-->B
In an imperative workflow, this might be expressed as:
step 1
step 2
if something changed
go back to step 1
From a reactive graph perspective, however, it becomes:
dependency changed
↓
affected execution becomes stale
↓
runtime reevaluates necessary work
This Isn't About Forcing Frontend Reactivity Into AI
There is one part of this idea that I think is easy to misunderstand.
If I say:
Use signals in an agent.
it sounds like I am taking a frontend library and trying to transplant it into AI.
But I now think the more useful way to frame the problem is:
Frontend was simply one of the first domains where reactive execution models were adopted at scale.
A typical frontend flow looks like this:
state changes
↓
derived state changes
↓
render changes
But if we remove the final step:
state changes
↓
derived computation changes
↓
dependent work changes
there is nothing inherently UI-specific about that model.
The more general concept is:
Dependency-driven execution
This is one reason I wanted signal-kernel to be independent of rendering frameworks from the beginning.
Only after separating Reactivity from Rendering can we really test the question:
Can reactivity work as a general-purpose execution model?
This Is Where Solid and signal-kernel Start to Diverge
The previous two articles used a lot of Solid 2.0's design as an example because Solid has pushed the integration of async computation into the reactive graph quite far.
Its direction can roughly be understood as:
Reactive Graph
↓
Async Execution
↓
Suspense / Transition
↓
Rendering
That is a very reasonable form of vertical integration.
Its goal is to make reactive execution and UI rendering work together more naturally.
The direction I chose is closer to:
Reactive Graph
↓
Async Runtime
↓
Execution Semantics
↓
Consumer
The consumer might be:
- React
- Vue
- Worker
- Server
- Agent
Neither direction is inherently more correct.
They simply draw the architectural boundary in different places.
Solid owns rendering, so it can directly integrate pending into:
Suspense / Transition
async-runtime does not own rendering, so concepts such as:
pending
stale
revision
cancel
invalidation
need to be defined as execution semantics in their own right.
That difference is what eventually pushes the two approaches toward different problem domains.
Reactive Execution Doesn't Require a UI
Let's return to the question that started this three-part series.
We began with:
createMemo(async () => ...)
The first article led to this idea:
Async pushes a reactive system from managing values toward managing executions.
The second led to:
An execution being pending and a rendering boundary being blocked are two different boundaries.
And now the third takes one more step:
If rendering is just a consumer, reactive execution itself does not need to be limited to UI.
A reactive runtime can begin to describe:
- which work depends on which state
- when work needs to execute again
- which results have become
stale - which execution is still valid
- who is allowed to cancel work
- which operations invalidate other data
These questions appear in:
- frontend systems
- server workflows
- background processing
- agent runtimes
So I have increasingly come to think of reactivity as:
A dependency-driven execution model.
Rendering is one extremely important and mature application of that model.
But it does not necessarily have to be the endpoint.
And once rendering is truly removed from the core, the next question becomes:
How should a general-purpose reactive runtime represent ownership, invalidation, and correctness across executions?
That is the direction I want to keep exploring next.
Top comments (5)
on my perspective of web api, reactivity just concept of running function on class instance, like useState in reat is factory with result of array object, then rename it as 'nameofstate' and 'setnameofstate', when we user web api, proxy, we binding set of function will be execute or we called trap. when we added render function so its end rendering, or when we added funtion to make change to other proxy, when I experiment with vanilla js, make proxy I added event listerer of localstorage event then set state so its "pseudo communication" betwen tab for sync state. the every tab state change render on their each tab browser.
Yeah, I think the cross-tab example is actually a good way to look at it. The state change and propagation can exist independently from rendering, while each tab can decide how to consume that change. That separation is very close to what I was trying to explore in this article.
I actually built reactive engine, its use web api, proxy, domparser and tree Walker. As I know since ES5 browser more strong and apply what framework simulator before, now old device obsolete, modern browser win the game. Reactive engine just for render, history api or baseline2026 navigation for router, and pure proxy if we wanna setup global state and multi tab sync
I think we're looking at reactivity at two different architectural levels.
What you're describing makes sense for a browser-centric reactive engine, where Proxy, DOM updates, routing, and shared state are the main concerns.
What I'm exploring in this article is a different boundary: whether dependency tracking and execution semantics need to belong to rendering at all.
In
signal-kernel, rendering is intentionally only one consumer. The runtime also needs to reason about things like invalidation, revisions, cancellation, streams, and superseded async executions, even when there is no DOM involved.So I don't think the question here is really about whether modern browser APIs are powerful enough to build a reactive engine. It's whether reactive execution itself has to be defined as a rendering mechanism.
Yeah its my already statement reactivity is function in class instance, in every enviromment, its pure my definition, etc class book, save([..book]) then inside save there was function to save db, to trap if author nameAuthor. Why we called framework "reactive framework" maybe because its setf of the way to specialized change DOM instant. Its primitive name was AJAX. Reactive framework was marketing brand. Simple as we code in many form function with sequential step