Headless UI engine: separating logic from rendering in SkyGraph
SkyGraph is my React UI library. I initially built it "normally" like shadcn or MUI: components with props, internal state, and JSX. A year in I realized this was a dead end.
Why "normal" architecture is a dead end
For complex features (100k-row table virtualization, drag-drop in trees, conditional form validation), logic ends up inside components — impossible to reuse or test properly.
Tests on JSX components = DOM tests: slow, brittle, teach nothing.
The headless solution
Headless = logic lives separately from rendering. The component subscribes to state and renders.
@skygraph/core — reactive runtime + 7 engines
@skygraph/react — React wrappers over core
@skygraph/vue — future Vue wrapper
Reactive runtime
At the core — a custom reactive runtime similar to Solid: signal, effect, computed. No object proxying, no VirtualDOM. Any piece of state is a signal you can subscribe to.
const count = signal(0);
effect(() => {
console.log("count:", count());
});
count.set(5);
Engines
On top of the runtime — 7 engines. Each is an independent abstraction with its own API:
- CoreEngine — common event bus, middleware, persistence
- FormEngine — field validation, dirty/touched, async validation
- TableEngine — sorting, filtering, selection, virtualization
- TreeEngine — node expansion, drag-drop, lazy loading
- VirtualEngine — rendering virtualization (shared by Table/List)
- GraphEngine — graphs, nodes, edges, layout algorithms
- CalendarEngine — date math, ranges, timezones
Why this works
When you need to test 100k-row table behavior — you write a TableEngine test without rendering. Runs in 5ms. When you need to port the library to Vue — you write wrappers, the core stays untouched.
Test coverage grew to 85%, 90% of which are core tests, no DOM.