DEV Community

Zastinian
Zastinian

Posted on

Hedystia 2.1: A Reactive UI Engine with No Virtual DOM

We just released Hedystia 2.1, and this time we're going full frontend with @hedystia/view — a fine-grained reactive UI engine that builds modern interfaces without a Virtual DOM.

If you've used Hedystia for the backend, you already know our philosophy: type safety, developer experience, and zero compromise on performance. Now we're bringing that same energy to the frontend.

TL;DR: @hedystia/view is a reactive UI framework where components run once, and reactivity updates only the exact DOM nodes that changed. No VDOM diffing. No reconciliation. Just surgical updates.

Install: bun add @hedystia/view
Docs: https://docs.hedystia.com/view/start

Why Build Another UI Framework?

We looked at the existing ecosystem and noticed something: most frameworks either use a Virtual DOM (React, Vue) or require a complete mental model shift (Solid, Svelte). We wanted something different:

  • No Virtual DOM — Create real DOM nodes directly
  • Components run once — No re-rendering, ever
  • Explicit reactivity — You control exactly what's reactive and what isn't
  • Type-safe end-to-end — Same type safety from backend to frontend
  • Familiar JSX — No new template syntax to learn

The Core Idea: Signals + JSX

At the heart of View is a signals system. Think of it as atomic reactive cells:

import { sig, val, set, update } from "@hedystia/view";

const count = sig(0);

console.log(val(count)); // 0
set(count, 1);
console.log(val(count)); // 1
Enter fullscreen mode Exit fullscreen mode

The magic happens in JSX with one simple pattern:

function Counter() {
  // This function runs ONCE when the component mounts.
  // Only the text node created by the reactive accessor updates.

  return (
    <div>
      {/* Static — read once during mount */}
      <h1>Counter</h1>

      {/* Reactive — creates a fine-grained effect */}
      <p>Count: {() => val(count)}</p>

      <button onClick={() => update(count, (c) => c + 1)}>
        Increment
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The rule: {val(signal)} is static. {() => val(signal)} is reactive. That's it. Apply this pattern to props, children, styles, class names — everything.

No Virtual DOM, No Overhead

When count changes, View doesn't diff a virtual tree or re-render the component. It updates exactly one text node. That's it.

This is possible because:

  1. Component functions execute once during mount
  2. Reactive accessors create effects that track their specific DOM nodes
  3. Signal dependencies trigger targeted updates — no reconciliation needed

The result? UIs that scale without the performance cliff of large re-renders.

Full Reactivity Toolkit

Batch & Untrack

Control when reactivity fires:

import { batch, untrack } from "@hedystia/view";

// Multiple updates, ONE reactive cycle
batch(() => {
  set(user.name, "Alice");
  set(user.email, "alice@example.com");
  set(user.age, 30);
});

// Read without creating a dependency
const snapshot = untrack(() => val(signal));
Enter fullscreen mode Exit fullscreen mode

Memo — Lazy Derived Values

const items = sig([1, 2, 3]);
const doubled = memo(() => val(items).map((x) => x * 2));
const sum = memo(() => val(doubled).reduce((a, b) => a + b, 0));

// Computed lazily — only when read
console.log(val(sum)); // 12
Enter fullscreen mode Exit fullscreen mode

Effects with Cleanup

import { on, once } from "@hedystia/view";

const dispose = on(
  () => val(userId),
  (id, prevId) => {
    console.log(`Switched from ${prevId} to ${id}`);
    // Return cleanup function
    return () => cleanupUser(id);
  }
);

// One-time effect
once(() => val(initialData), (data) => {
  console.log("Loaded:", data);
});
Enter fullscreen mode Exit fullscreen mode

Store — Nested Reactive State

For complex state, use the proxy-based store:

import { store, val, set, patch, snap } from "@hedystia/view";

const user = store({
  name: "Alice",
  address: { city: "Wonderland" },
});

val(user.name);              // "Alice"
val(user.address.city);      // "Wonderland"
set(user.address.city, "NYC");

// Deep partial update
patch(user.address, { city: "Los Angeles" });

// Snapshot to plain object
const copy = snap(user);
Enter fullscreen mode Exit fullscreen mode

Flow Control Components

Conditional Rendering

<Show when={() => val(isLoggedIn)} fallback={<Login />}>
  <Dashboard />
</Show>
Enter fullscreen mode Exit fullscreen mode

Multi-way Conditionals

<Switch>
  <Match when={() => val(status) === "loading"}><Spinner /></Match>
  <Match when={() => val(status) === "error"}><Error /></Match>
  <Match when={() => val(status) === "success"}><Data /></Match>
</Switch>
Enter fullscreen mode Exit fullscreen mode

Keyed Lists

<For each={() => val(users)} key={(u) => u.id}>
  {(user) => <UserCard user={user} />}
</For>
Enter fullscreen mode Exit fullscreen mode

Index-Based Lists

<Index each={() => val(items)}>
  {(item, index) => <div>#{index}: {() => val(item)}</div>}
</Index>
Enter fullscreen mode Exit fullscreen mode

Portal

<Portal>
  <Modal>Content outside hierarchy</Modal>
</Portal>
Enter fullscreen mode Exit fullscreen mode

Data Fetching Built In

Load — Reactive Queries

const userId = sig(1);

const user = load(
  () => `user-${val(userId)}`,  // Key triggers refetch
  async () => {
    const res = await fetch(`/api/users/${val(userId)}`);
    return res.json();
  }
);

// user.data, user.loading, user.error, user.ready
Enter fullscreen mode Exit fullscreen mode

Auto-refetches when the key changes. Auto-aborts previous in-flight requests.

Action — Reactive Mutations

const createPost = action(async (data) => {
  return fetch("/api/posts", { method: "POST", body: JSON.stringify(data) })
    .then((r) => r.json());
});

createPost.run({ title: "Hello" });
// createPost.loading, createPost.error, createPost.data
Enter fullscreen mode Exit fullscreen mode

Context — Dependency Injection

import { ctx, use, Context } from "@hedystia/view";

const themeCtx = ctx<"light" | "dark">("light");

function App() {
  return (
    <Context.Provider value={themeCtx} value={() => "dark"}>
      <Header />
    </Context.Provider>
  );
}

function Header() {
  const theme = use(themeCtx); // "dark"
  return <header class={val(theme)}>Themed</header>;
}
Enter fullscreen mode Exit fullscreen mode

Lifecycle Hooks

function MyComponent() {
  onMount(() => {
    console.log("Mounted");
    return () => console.log("Cleanup");
  });

  onReady(() => {
    // After first render — perfect for focus/measurements
    inputRef.focus();
  });

  onCleanup(() => {
    // Alternative cleanup registration
  });

  return <input ref={(el) => (inputRef = el)} />;
}
Enter fullscreen mode Exit fullscreen mode

Rendering & SSR

import { mount, renderToString } from "@hedystia/view";

// Mount to browser
const app = mount(<App />, document.getElementById("app")!);
app.dispose(); // Unmount

// Server-side rendering
const html = renderToString(<App />);
Enter fullscreen mode Exit fullscreen mode

The Performance Difference

Because View doesn't use a Virtual DOM:

  • No diffing overhead — Updates go straight to the DOM
  • No reconciliation — No tree traversal to find what changed
  • Predictable performance — O(1) updates regardless of tree size
  • Lower memory — No virtual tree to maintain

For small apps the difference is negligible. For complex dashboards, data tables, and real-time UIs, it's noticeable.

What Else Is New?

Hedystia 2.1 also includes all the features from our recent releases:

  • 2.0@hedystia/db: Type-safe ORM with MySQL, SQLite, and File support, smart caching, and migrations
  • 1.10 — Route testing, conditional routes, WebSocket support, and framework-agnostic type generation

Getting Started

# Install
bun add @hedystia/view

# Or with the full framework
bun add hedystia @hedystia/view
Enter fullscreen mode Exit fullscreen mode

Check out the interactive docs at https://docs.hedystia.com/view/start

What's Next?

We're working on:

  • DevTools for signal debugging
  • Transition/animation system
  • More flow control components
  • Performance profiling utilities

We'd love your feedback! Try it out, break things, and let us know what you think on Discord or GitHub.


Hedystia is an open-source TypeScript backend framework now with a full-stack reactive UI engine. Type-safe from database to DOM.

Top comments (0)