One engine for React and Vue: how SkyGraph works across frameworks
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:
- SSR — Web Components work poorly with server-side rendering
- Typing — props via attributes (strings), no type-safe API
- Styles — Shadow DOM complicates customization
- 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