Headless UI-движок: разделение логики и рендера в SkyGraph
SkyGraph — моя UI-библиотека для React. Изначально я делал её "обычной", как shadcn или MUI: компонент состоит из пропсов, внутреннего состояния и JSX. Через год понял что это тупик.
Почему обычная архитектура — тупик
Когда нужна сложная фича (виртуализация таблицы на 100к строк, drag-and-drop в дереве, валидация формы с условными полями), внутри компонента появляется логика, которую невозможно ни переиспользовать, ни нормально протестировать.
Тесты на JSX-компонент = тесты на DOM, что медленно, хрупко и не учит ничему важному.
Решение — headless
Headless значит: вся логика живёт отдельно от рендера. Компонент только подписывается на состояние и рисует.
Структура SkyGraph:
@skygraph/core — reactive runtime + 7 engines
@skygraph/react — React-обёртки над core
@skygraph/vue — будущая Vue-обёртка
Reactive runtime
В основе — самописный reactive runtime похожий на Solid: signal, effect, computed. Без проксирования объектов, без VirtualDOM. Любая часть состояния — это signal, на который можно подписаться.
const count = signal(0);
effect(() => {
console.log("count:", count());
});
count.set(5);
Engines
Поверх runtime — 7 движков. Каждый — независимая абстракция со своим API:
- CoreEngine — общая шина событий, middleware, persistence
- FormEngine — валидация полей, dirty/touched, asynchronous validation
- TableEngine — сортировка, фильтрация, выделение, виртуализация
- TreeEngine — раскрытие узлов, drag-drop, lazy loading
- VirtualEngine — виртуализация рендера (общий движок для Table/List)
- GraphEngine — графы, узлы, рёбра, layout-алгоритмы
- CalendarEngine — date-математика, диапазоны, timezones
Почему это работает
Когда нужно протестировать поведение таблицы на 100к строк — пишешь тест на TableEngine без рендера. Запускается за 5мс. Когда нужно перевести библиотеку на Vue — пишешь обёртки, ядро не трогаешь.
Тестовое покрытие выросло до 85%, из них 90% — тесты на ядро, без DOM.