When I first started building signal-kernel, I thought I was simply writing a signal library—a fine-grained reactive system not tied to any specific UI framework. On the surface, it had all the basic building blocks you would expect from a signal library:
-
Core primitives:
Signal,Computed, andEffect - Mechanisms: dependency tracking and scheduling
- Extensions: async resources and framework adapters
It could build a reactive graph, track dependencies, update derived state precisely when data changed, and bridge the result into React or Vue. But the deeper I dug into the implementation, the more I realized the real problem wasn't about building another signal library. The core question was actually this:
In a complex application, who should truly own the dataflow?
Fragmented Dataflow and the Derived State Problem
In modern frontend applications, dataflow often feels natural at first. API data comes back, gets written into state, and components render the result. If we need derived data, we use useMemo. If we need side effects, we use useEffect. If we need async requests, we use a query library, and if we need shared state, we introduce a store.
Individually, none of these solutions are wrong. React is not wrong. Vue is not wrong. TanStack Query is not wrong. Zustand and Jotai are not wrong either. The real problem appears when the system grows larger. At that point, the exact same logical dataflow starts getting split across multiple ownership models.
Some state belongs to components, while other state belongs to a store. Some derived state is trapped inside the render phase, and async state lives inside a query cache. The system still works, but the boundaries of the dataflow start to depend heavily on conventions, team discipline, and developer memory. It becomes incredibly hard to answer one seemingly simple question: Who actually owns this derived state? Is it the component, the store, the framework runtime, or just a temporary value calculated inside a hook?
Render Should Not Naturally Own the Entire Dataflow
Frontend developers often treat render as the center of the system. This is completely understandable—what we look at and debug every day is the UI. So when data changes, our first instinct is usually: "Which component needs to re-render?"
But if we reverse the question, the architecture looks very different. When data changes, perhaps the first question should be: "Which pieces of derived data are actually affected?" Render is simply one kind of side effect caused by data changes. It is extremely important, but it should not naturally own the entire dataflow.
This became one of the core architectural lessons I learned:
The reactive graph should first describe the relationships between data, while render should only be one consumer of that graph.
From Signal Library to Dataflow Kernel
If I only saw signal-kernel as a signal library, then most design questions would stay at the API surface: Should it feel more like Solid? Should computed values be lazy or eager? Should it support batching? These are important, but they hide the deeper issue: When a piece of state changes, who is responsible for deciding which derived states are affected?
In a traditional component-driven model, this usually happens during render:
Data changes → component re-executes → derived data is recalculated.
This model works beautifully for small applications, but it forces the lifecycle of derived state to follow the lifecycle of render. Once derived state becomes expensive, needs to be shared across frameworks, or needs to be deeply connected with async streams, this model becomes fragile.
That is why the real goal of signal-kernel was never simply to “reduce re-renders.” The goal was to detach derived dataflow from render ownership. What I actually wanted to build was a dataflow kernel.
A system that focuses on lower-level architectural concerns:
- How state becomes a pure source of truth.
- How derived state is tracked stably inside a reactive graph.
- How effects can be separated cleanly from the render lifecycle.
- How async work and continuously written streams can become part of the reactive graph while keeping clear state boundaries.
- How render can return to its proper role: a consumer, not the owner.
Keeping the Boundary: Thin Framework Adapters
This way of thinking also directly shaped how I designed the React and Vue adapters. At the beginning, I naturally wanted to provide many convenient hooks and APIs that felt familiar to framework users. But over time, I became much more conservative.
If an adapter becomes too thick, it quietly pulls ownership back into the UI framework. And once that happens, the data independence that signal-kernel is trying to preserve starts to fade away.
A cleaner and more stable boundary should look like this:
- Core owns the reactive graph.
- Async-runtime owns the consistency of async state.
- Framework adapters only subscribe to and read snapshots.
- Render only draws the current result to the screen.
An adapter should not redefine the dataflow. It should only be a transparent pipe into the framework.
Rethinking Framework Responsibility
I want to make one thing clear: this series is not meant to criticize React or any other UI tool. React is still excellent at handling UI consistency, and TanStack Query solves a very real, difficult server-state problem.
What I want to explore is a deeper architectural question:
When we habitually put all data logic into the model of a UI framework, are we accidentally making render responsible for too much?
If reactive dataflow itself can be an independent system, then what role should a UI framework actually play?
What I Was Really Studying Was Ownership
While implementing the core and async-runtime of signal-kernel, I kept running into a series of uncomfortable architectural questions:
- After a Promise resolves, is it really just a simple
setState? - If a stream keeps emitting chunks, who should own its stable value, status, and error?
- After a mutation succeeds, is invalidation the responsibility of the query cache, or the reactive graph?
All of these questions eventually point to the same word: Ownership.
I originally thought I was building a signal library. But the more I built, the more I realized that what I was really studying was how the boundaries of dataflow should be redefined in increasingly complex frontend applications. APIs can change. Implementations can be replaced. But the core question remains the same:
When data changes, who is responsible for understanding that change?
If the answer is always render, then we are forced to keep layering workarounds around the UI framework. But if the answer is the reactive graph, then render can return to its most reasonable position. It is one outlet of the dataflow. It is one side effect. It is a consumer of the system, not the owner.
That is the most important lesson I learned while building signal-kernel. If this idea resonates with you, I’m exploring it through signal-kernel, a framework-agnostic reactive dataflow kernel.
The project is still evolving, but its direction is clear: render should be a consumer of the dataflow, not the owner.
In the next article, I want to continue exploring this question:
What actually changes in frontend architecture when render no longer owns the entire dataflow?
Top comments (0)