RS

One engine for React and Vue: how SkyGraph works across frameworks

Invalid Date min read

Most UI libraries are tied to a single framework. MUI — React. Vuetify — Vue. If a team migrates from React to Vue (or vice versa), the library has to change. SkyGraph solves this differently: one engine, two adapters.

Architecture

@skygraph/core     — reactive runtime + engines (framework-agnostic)
@skygraph/react    — React wrappers
@skygraph/vue      — Vue wrappers
@skygraph/css      — shared styles (CSS Variables)

The core (@skygraph/core) knows nothing about React or Vue. It exports engines: TableEngine, FormEngine, TreeEngine, CalendarEngine, and others. Each engine is a class with reactive signals inside.

Reactive signals

Instead of React state or Vue refs — custom 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);
});

A signal is an observable value. computed is a derived value that recomputes when dependencies change. effect is a side effect that runs on change.

React adapter

The React adapter subscribes to signals via 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 adapter

The Vue adapter wraps signals in 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;
}

A Vue component gets the same table, the same API, the same features — through the familiar Composition API.

CSS without duplication

Styles are shared across both frameworks — plain CSS with 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;
}

Theme switching — one attribute on <html>. No CSS-in-JS, no runtime styles, no tree-shaking issues.

Why not Web Components

The alternative approach — Web Components (Lit, Stencil). Rejected for:

  1. SSR — Web Components work poorly with server-side rendering
  2. Typing — props via attributes (strings), no type-safe API
  3. Styles — Shadow DOM complicates customization
  4. DX — developers want familiar framework patterns

Result

  • Styled components for React and Vue on the same core
  • One PR to core = feature in both frameworks simultaneously
  • Core tests — 85% coverage, no DOM
  • Migration between frameworks: change the import, logic stays the same