Hedystia 2.1 - Reactive UI Engine
Introducing @hedystia/view: A Fine-Grained Reactive UI Framework with No Virtual DOM
Hedystia 2.1 brings frontend development into the picture with @hedystia/view, a brand-new reactive UI engine that lets you build modern, performant user interfaces with the same type-safe developer experience you love from the backend.
🎨 @hedystia/view — A New Way to Build UIs
We built @hedystia/view from the ground up with a simple philosophy: components run once, reactivity is surgical. Instead of re-rendering entire component trees, View updates only the exact DOM nodes that depend on changed values.
bun add @hedystia/view⚡ No Virtual DOM, No Overhead
Unlike traditional UI frameworks, @hedystia/view creates real DOM nodes directly. There's no virtual DOM diffing, no reconciliation overhead — just precise, efficient updates exactly where they're needed.
import { sig, val, set } from "@hedystia/view";
const count = sig(0);
function Counter() {
// This component runs ONCE
// Only the text node updates when count changes
return (
<div>
<p>Count: {() => val(count)}</p>
<button onClick={() => set(count, val(count) + 1)}>
Increment
</button>
</div>
);
}The key pattern: {val(count)} is static (read once), while {() => val(count)} is reactive (creates a fine-grained effect).
📡 Signals — The Foundation of Reactivity
At the heart of View lies a powerful signals system that gives you explicit control over reactivity:
import { sig, val, set, update, peek, memo } from "@hedystia/view";
// Create a signal
const name = sig("Alice");
// Read and write
console.log(val(name)); // "Alice"
set(name, "Bob");
update(name, (n) => n.toUpperCase());
// Peek without creating dependencies
const snapshot = peek(name);
// Create derived values
const greeting = memo(() => `Hello, ${val(name)}!`);Signals support custom equality comparators, batch updates, and untracked reads for maximum flexibility.
Batch & Untrack
Control when and how reactivity triggers:
import { batch, untrack } from "@hedystia/view";
// Defer notifications until batch completes
batch(() => {
set(user.name, "Alice");
set(user.age, 30);
set(user.email, "alice@example.com");
// Only ONE reactive cycle triggers
});
// Read without creating dependencies
const copy = untrack(() => val(signal));Memo — Lazy Derived Values
Memos compute values on-demand and only re-evaluate when dependencies actually change:
const items = sig([1, 2, 3, 4, 5]);
const doubled = memo(() => val(items).map((x) => x * 2));
const sum = memo(() => val(doubled).reduce((a, b) => a + b, 0));
// Values are computed lazily when read, not eagerly
console.log(val(sum)); // 30🎯 Effects — Reactive Side Effects
Perform reactive operations with explicit control over dependencies and cleanup:
import { on, once } from "@hedystia/view";
// Reactive effect with explicit tracking
const dispose = on(
() => val(userId),
(id, prevId) => {
console.log(`User changed from ${prevId} to ${id}`);
// Return cleanup function
return () => {
console.log(`Cleaning up user ${id}`);
};
}
);
// Run once and auto-dispose
once(
() => val(initialData),
(data) => {
console.log("Initial data loaded:", data);
}
);🧩 JSX — Build UIs with Familiar Syntax
View uses JSX with a custom runtime (jsxImportSource: "@hedystia/view"). The same static vs reactive pattern applies everywhere:
import { sig, val, set } from "@hedystia/view";
import { Show } from "@hedystia/view";
function App() {
const visible = sig(true);
const theme = sig("dark");
return (
<div className={() => `theme-${val(theme)}`}>
{/* Static: evaluated once */}
<h1>Welcome</h1>
{/* Reactive: updates when theme changes */}
<p class={() => val(theme) === "dark" ? "text-white" : "text-black"}>
Current theme: {() => val(theme)}
</p>
{/* Reactive children */}
<Show when={() => val(visible)}>
<p>This is visible!</p>
</Show>
<button onClick={() => set(visible, !val(visible))}>
Toggle
</button>
</div>
);
}Event handlers use standard on<EventName> syntax, both class and className work, and ref gives you direct DOM access after insertion.
🔄 Flow Control — Conditional & List Rendering
Show — Conditional Rendering
<Show when={() => val(isLoggedIn)} fallback={<LoginPrompt />}>
<Dashboard />
</Show>For — Keyed List Rendering
DOM nodes move with their data based on key identity:
<For each={() => val(users)} key={(user) => user.id}>
{(user) => (
<div>
<span>{user.name}</span>
<span>{user.email}</span>
</div>
)}
</For>Index — Index-Based List Rendering
DOM nodes stay in place; content updates when data at that index changes:
<Index each={() => val(items)}>
{(item, index) => (
<div>
#{index}: {() => val(item)}
</div>
)}
</Index>Switch & Match — Multi-Way Conditionals
<Switch>
<Match when={() => val(status) === "loading"}>
<Spinner />
</Match>
<Match when={() => val(status) === "error"}>
<ErrorMessage />
</Match>
<Match when={() => val(status) === "success"}>
<DataDisplay />
</Match>
<Fallback>
<UnknownState />
</Fallback>
</Switch>Portal — Render Outside Hierarchy
Perfect for modals, tooltips, and dropdowns:
<Portal>
<div class="modal-overlay">
<div class="modal">Content here</div>
</div>
</Portal>Defaults to document.body, or specify a custom mount point.
🏪 Store — Nested Reactive State
Manage complex nested state with an intuitive proxy-based API:
import { store, val, set, patch, reset, snap } from "@hedystia/view";
const user = store({
name: "Alice",
address: {
city: "Wonderland",
zip: "12345",
},
preferences: {
theme: "dark",
notifications: true,
},
});
// Access with dot notation (leaf values are signals)
console.log(val(user.name));
console.log(val(user.address.city));
// Set nested values
set(user.name, "Bob");
set(user.address.city, "New York");
// Deep partial update
patch(user.address, { city: "Los Angeles" });
patch(user.preferences, { theme: "light" });
// Snapshot to plain object
const snapshot = snap(user);
// Reset to initial state
reset(user);🌐 Context — Dependency Injection
Pass values through the component tree without prop drilling:
import { ctx, use, Context } from "@hedystia/view";
const themeCtx = ctx<"light" | "dark">("light");
function App() {
return (
<Context.Provider value={themeCtx} value={() => "dark" as const}>
<Header />
<Content />
</Context.Provider>
);
}
function Header() {
const theme = use(themeCtx); // "dark"
return <header class={val(theme)}>Themed Header</header>;
}Contexts are fully typed and throw if consumed without a provider (unless a default is provided).
📦 Fetch — Reactive Data Fetching
Built-in utilities for data fetching with loading states, error handling, and auto-refetch:
Load — Reactive Queries
import { sig, val, set, load } from "@hedystia/view";
const userId = sig(1);
function UserProfile() {
const user = load(
() => val(userId),
async () => {
const res = await fetch(`/api/users/${val(userId)}`);
return res.json();
}
);
return (
<Show when={() => val(user.ready)}>
<div>
<h2>{() => val(user.data).name}</h2>
<p>{() => val(user.data).email}</p>
</div>
</Show>
);
}The load function provides: data, loading, error, state (pending/ready/refreshing/errored), and ready. It auto-aborts previous fetches when the key changes.
Action — Reactive Mutations
import { sig, val, set, action } from "@hedystia/view";
function CreatePost() {
const createPost = action(async (postData: PostData) => {
const res = await fetch("/api/posts", {
method: "POST",
body: JSON.stringify(postData),
});
return res.json();
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
createPost.run({ title: "New Post", content: "..." });
}}
>
<Show when={() => val(createPost.loading)}>
<p>Creating post...</p>
</Show>
<Show when={() => val(createPost.data)}>
<p>Post created: {() => val(createPost.data).id}</p>
</Show>
<button disabled={val(createPost.loading)}>Create</button>
</form>
);
}🎭 Lifecycle — Mount, Cleanup, and Ready
Control component lifecycle with precision:
import { onMount, onCleanup, onReady } from "@hedystia/view";
function MyComponent() {
onMount(() => {
console.log("Component mounted");
// Return cleanup or use onCleanup
return () => {
console.log("Component unmounting");
};
});
onCleanup(() => {
console.log("Cleanup registered");
});
onReady(() => {
// Runs after first render via queueMicrotask
// Perfect for DOM measurements or auto-focus
inputRef.focus();
});
return <input ref={(el) => (inputRef = el)} />;
}🖼️ Rendering — Mount, Dispose, and SSR
import { mount, renderToString } from "@hedystia/view";
// Mount to DOM
const app = mount(<App />, document.getElementById("app")!);
// Dispose (unmount)
app.dispose();
// Server-side rendering
const html = renderToString(<App />);renderToString produces a one-time HTML snapshot with no reactivity — perfect for SSR.
🎨 Style Utilities
Style helpers for computed and merged styles:
import { style, merge } from "@hedystia/view";
// Computed style (static or reactive)
const buttonStyle = style({
padding: "12px 24px",
borderRadius: "8px",
background: () => (val(isPrimary) ? "blue" : "gray"),
});
// Merge styles
const merged = merge(baseStyle, buttonStyle, additionalStyle);
<div style={merged}>Styled Element</div>;📐 Text Measurement
Measure and layout text reactively using the Canvas API:
import { prepare, layout, reactiveLayout } from "@hedystia/view";
// Measure text dimensions
const measured = prepare("Hello, World!", "16px sans-serif");
console.log(measured.width, measured.height);
// Compute line wrapping
const lines = layout(measured, 300, 1.5);
// Reactive layout (memoized)
const reactiveLines = reactiveLayout(
() => val(textContent),
() => prepare(val(textContent), "16px sans-serif"),
300,
1.5
);⏱️ Scheduler — Frame-Aware Updates
Schedule work with requestAnimationFrame for smooth animations:
import { tick, nextFrame, forceFlush } from "@hedystia/view";
// Schedule for next animation frame
tick(() => {
console.log("Running in next frame");
});
// Wait for next frame
await nextFrame();
console.log("After frame");
// Force flush (for testing)
forceFlush();🎯 Summary
Hedystia 2.1 introduces a complete reactive UI engine:
- No Virtual DOM — Real DOM nodes with surgical updates
- Components Run Once — No re-rendering, just signal-driven effects
- Fine-Grained Reactivity — Signals, memos, effects, and stores
- Full JSX Support — Custom runtime with reactive patterns
- Data Fetching — Built-in
loadandactionfor queries and mutations - Flow Control — Show, For, Index, Switch/Match, Portal
- Context & Store — Dependency injection and nested reactive state
- SSR Support —
renderToStringfor server rendering - Utilities — Scheduler, style helpers, text measurement
The View Philosophy
View is built on a simple principle: reactivity should be explicit and performant. Components execute once. Signals track dependencies. Only the DOM nodes that depend on changed values update. There's no virtual DOM diffing, no reconciliation — just direct, efficient updates.
This is the same philosophy Hedystia brings to the backend: type safety, developer experience, and performance without compromise.
Install
bun add @hedystia/viewFull Documentation
Explore the complete documentation at View — Getting Started.
Thank you to everyone in the Hedystia community for your feedback and support. We're excited to see what you build! 🚀
