RS

Один движок на React и Vue: как SkyGraph работает в двух фреймворках

Invalid Date мин чтения

Большинство UI-библиотек привязаны к одному фреймворку. MUI — React. Vuetify — Vue. Если команда мигрирует с React на Vue (или наоборот), библиотеку приходится менять. SkyGraph решает это иначе: один движок, два адаптера.

Архитектура

@skygraph/core     — reactive runtime + engines (framework-agnostic)
@skygraph/react    — React-обёртки
@skygraph/vue      — Vue-обёртки
@skygraph/css      — общие стили (CSS Variables)

Ядро (@skygraph/core) не знает ничего о React или Vue. Оно экспортирует движки: TableEngine, FormEngine, TreeEngine, CalendarEngine и другие. Каждый движок — это класс с reactive signals внутри.

Reactive signals

Вместо React state или Vue refs — собственные signals:

import { signal, computed, effect } from "@skygraph/core";

const rows = signal<Row[]>([]);
const sortedRows = computed(() => {
  return [...rows()].sort(compareFn);
});

effect(() => {
  console.log("Rows changed:", sortedRows().length);
});

Signal — это observable-значение. computed — derived-значение, пересчитывается при изменении зависимостей. effect — побочный эффект, выполняется при изменении.

React-адаптер

React-адаптер подписывается на signals через useSyncExternalStore:

function useSignal<T>(sig: Signal<T>): T {
  return useSyncExternalStore(
    sig.subscribe,
    sig.get,
    sig.get
  );
}

export function SkyTable({ data, columns }: SkyTableProps) {
  const engine = useMemo(() => new TableEngine({ data, columns }), []);
  const sorted = useSignal(engine.sortedRows);
  const selected = useSignal(engine.selectedIds);

  return <table>...</table>;
}

Vue-адаптер

Vue-адаптер оборачивает signals в shallowRef + watchEffect:

function useSignal<T>(sig: Signal<T>): ShallowRef<T> {
  const ref = shallowRef(sig.get());

  watchEffect((onCleanup) => {
    const unsub = sig.subscribe((val) => { ref.value = val; });
    onCleanup(unsub);
  });

  return ref;
}

Компонент на Vue получает ту же таблицу, тот же API, те же features — но через привычный Composition API.

CSS без дублирования

Стили общие для обоих фреймворков — чистый CSS с CSS Variables:

[data-sky-theme="light"] {
  --sky-bg: #ffffff;
  --sky-border: #e2e8f0;
  --sky-text: #1a202c;
}

[data-sky-theme="dark"] {
  --sky-bg: #1a1a2e;
  --sky-border: #2d3748;
  --sky-text: #e2e8f0;
}

Переключение темы — один атрибут на <html>. Нет CSS-in-JS, нет runtime стилей, нет tree-shaking проблем.

Почему не Web Components

Альтернативный подход — Web Components (Lit, Stencil). Отвергнут по причинам:

  1. SSR — Web Components плохо работают с серверным рендерингом
  2. Типизация — пропсы через атрибуты (строки), нет type-safe API
  3. Стили — Shadow DOM усложняет кастомизацию
  4. DX — разработчики хотят использовать привычные паттерны фреймворка

Результат

  • Стилизованные компоненты для React и Vue на одном ядре
  • Один PR в ядро = фича в обоих фреймворках одновременно
  • Тесты ядра — 85% coverage, без DOM
  • Миграция между фреймворками: меняешь импорт, логика та же