@rabbx/fx
v1.2.0
Published
JSX that compiles to raw DOM. Signals that update exactly what changed. Zero virtual DOM.
Readme
@rabbx/fx
JSX that compiles to raw DOM. Signals that update exactly what changed. Zero virtual DOM.
⚡ What is @rabbx/fx?
@rabbx/fx is a fine-grained reactive framework that compiles JSX directly into imperative DOM operations at build time. There is no virtual DOM, no diffing algorithm, and no runtime template compiler. When a signal changes, only the exact text node, attribute, or element bound to that signal updates — nothing else is touched.
Write familiar JSX. Get Svelte-level performance. Ship ~3kb of runtime.
const count = $state(0);
return (
<button onclick={() => { count.value += 1; }}>
Clicked {count.value} times
</button>
);
// Compiles to:
// const el = _el("button");
// on(el, "click", () => { count.value += 1; });
// append(el, () => `Clicked ${count.value} times`);🚀 Cool Features
| Feature | Description |
|---|---|
| Compile-Time Macros | <For>, <Each>, <Show> are erased at build time. Zero runtime cost for control flow. |
| Keyed Reconciliation | <For> uses O(n) keyed diffing via reconcileKeyed(). Preserves focus, scroll, and animations. |
| Svelte-Style Directives | use:tooltip={{ text }} with auto-cleanup and reactive param detection. |
| Reactive Array Chains | .filter().map() compiles to a single $effect with keyed output. No intermediate allocations. |
| Partial Hydration Islands | Embed interactive components in static SSR HTML. Each island hydrates independently. |
| Replace-on-Hydrate | Client renders fresh DOM off-screen, then atomically swaps SSR content. Zero cursor-alignment bugs. |
| Zero-Styling Primitives | FxButton, FxInput, FxFetch etc. ship with no CSS. Full attribute pass-through. Your design system, your rules. |
| Context Without Traversal | Module-scoped signal stacks. No provider wrapper elements in output. O(1) lookup. |
| SSR That Actually Works | All macros, directives, and signals produce correct server HTML. Inert on server, alive on client. |
📦 Installation
npm install @rabbx/fxVite Plugin
// vite.config.ts
import { defineConfig } from 'vite';
import { rabbxPlugin } from '@rabbx/fx/vite';
export default defineConfig({
plugins: [rabbxPlugin()],
});🎯 Quick Start
export default function Counter() {
const count = $state(0);
const doubled = $computed(()=>count.value*2);
// Runs whenever `count` changes
$effect(() => {
document.title = `Count: ${count.value}`;
// Optional cleanup
return () => {
document.title = 'Rabbx App';
};
});
return (
<div class="p-4 space-y-3 shadow-2xl w-50 h-40 ">
<FxReturn value={()=>(
<p class="text-gray-700">Count: {count.value} | Doubled: {doubled.value}</p>
)} deps={[count.value,doubled.value]}/>
<button
class="px-4 py-2 bg-blue-600 text-white rounded cursor-pointer"
onclick={() =>{count.set(c => c + 1)}}
>
Increment
</button>
</div>
);
}📖 API Reference
Signals
| Primitive | Signature | Description |
|---|---|---|
| $state<T> | (init: T) → Signal<T> | Mutable signal. Read/write via .value. Functional update via .set(fn). |
| $computed<T> | (fn: () → T) → Signal<T> | Derived signal. Auto-tracks dependencies. Lazy evaluation. |
| $effect | (fn: EffectFn) → () → void | Side effect. Re-runs when tracked signals change. Returns dispose fn. |
| $effect.root | (fn: EffectFn) → () → void | Untracked root scope. Collects nested cleanups. |
| $effect.defer | (fn: EffectFn) → () → void | Microtask-deferred effect. Avoids blocking initial render. |
| $bindable<T> | (init: T, cb?) → Signal<T> | Two-way binding signal with optional change callback. |
| $resource<T> | (fetcher: () → Promise<T>) → Resource<T> | Async data fetching. Returns { data, loading, error } signals. |
| untracked | (fn: () → T) → T | Read signals without subscribing to current effect. |
| batch | (fn: () → void) → void | Group multiple writes into single flush cycle. |
Compiler Macros
Macros are transformed at build time. They produce zero runtime overhead.
<For> — Reactive Keyed List
<For items={todos.value} as="todo">
<li key={todo.id}>
<span>{todo.text}</span>
<button onclick={() => remove(todo.id)}>✕</button>
</li>
</For>Compiles to $effect + reconcileKeyed(). Re-renders only changed items. Preserves DOM identity.
<Each> — Static List
<Each items={navItems} as="item">
<a href={item.href}>{item.label}</a>
</Each>Compiles to raw for loop. Zero reactivity overhead. Ideal for static data.
<Show> — Conditional Rendering
<Show when={isLoggedIn.value} fallback={<LoginForm />}>
<Dashboard />
</Show>Compiles to $effect + if/else. Wrapper uses display:contents for zero layout impact.
<Suspense> — Async Boundary
<Suspense fallback={<Spinner />}>
<FxFetch url="/api/data">{(data) => <DataView data={data} />}</FxFetch>
</Suspense>Directives
Svelte-style use: directives attach behavior to DOM elements.
<button use:tooltip={{ text: 'Save' }}>Save</button>
<input use:focus />
<div use:clickOutside={() => { open.value = false; }}>...</div>- Actions run after element is appended to DOM
- Cleanup registered automatically via
onDestroy - Params containing
.valueare wrapped in reactive getters - SSR-safe: actions never execute on server
Zero-overhead. Module-scoped signal stacks. No VDOM traversal. No wrapper elements in output.
Store
Global reactive state with external pub/sub.
import { createStore } from '@rabbx/fx/runtime/store';
export const userStore = createStore({ name: '', theme: 'dark' });
// Auto-tracks in components
<FxReturn value={() => <span>{userStore.value.name}</span>} deps={[userStore.value]} />
// External subscriber (persistence, logging)
const unsub = userStore.subscribe(v => localStorage.setItem('user', JSON.stringify(v)));
// Functional update
userStore.set(prev => ({ ...prev, theme: 'light' }));Fx Primitives
All primitives are unstyled. Full attribute pass-through via [key: string]: any.
| Component | Purpose |
|---|---|
| FxButton | Button with pass-through attrs |
| FxInput / FxTextArea | Input primitives with event support |
| FxForm | Form with preventDefault default |
| FxLink | Anchor primitive |
| FxText | Text with optional tag wrapper |
| FxImage | Image with onload/onerror |
| FxHtml | Raw HTML injection with optional sanitization |
| FxFetch | Declarative fetch with children/fallback/error callbacks |
| FxAwait | Declarative promise resolution |
| FxEffect | Declarative $effect wrapper |
| FxReturn | Render delegation with optional reactive deps |
| FxShow / FxIf / FxElse | Conditional rendering primitives |
Islands (Partial Hydration)
import { registerIsland, bootstrapIslands } from '@rabbx/fx/client/bootstrap-islands';
// Register
registerIsland('Counter', () => import('./islands/Counter.js'));
bootstrapIslands();
// Use in page
<Island islandId="Counter" component={Counter} strategy="visible" props={{ initial: 42 }} />| Strategy | Behavior |
|---|---|
| eager / immediate | Hydrate on microtask after paint |
| visible | Hydrate when entering viewport (200px margin) |
| idle | Hydrate during browser idle time |
🔥 Canonical Syntax Reference
This is the definitive pattern for all @rabbx/fx components:
import { $state, $computed } from '@rabbx/fx/runtime';
interface Todo { id: number; text: string; done: boolean }
export default function TodoList() {
const todos = $state<Todo[]>([...]);
const inputVal = $bindable('');
let nextId = 4;
const remaining = $computed(() => todos.value.filter(t => !t.done).length);
const total = $computed(() => todos.value.length);
function addTodo() { /* ... */ }
function toggleTodo(id: number) { /* ... */ }
function removeTodo(id: number) { /* ... */ }
function renderHeader() {
return (
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white">Tasks</h2>
<span class="text-sm font-mono text-gray-500">
{remaining.value}/{total.value} remaining
</span>
</div>
);
}
function strikeTodo(todo: Todo) {
return (
<button
class={`w-5 h-5 rounded border flex items-center justify-center text-xs cursor-pointer transition-colors shrink-0 ${
todo.done ? 'bg-blue-500 border-blue-500 text-white' : 'border-gray-600 text-transparent hover:border-gray-400'
}`}
onclick={() => toggleTodo(todo.id)}
>{todo.done ? '✓' : ''}</button>
);
}
return (
<div class="max-w-lg mx-auto p-6 bg-[#1e1f22] border border-white/10 rounded-xl space-y-4">
<FxReturn value={renderHeader} deps={[remaining.value, total.value]} />
<div class="flex gap-2">
<input
type="text" placeholder="Add a task…"
class="flex-1 bg-[#0e0e10] border border-white/10 rounded-lg px-4 py-2 text-sm text-white outline-none focus:border-blue-500"
value={inputVal.value}
oninput={(e: Event) => { inputVal.value = (e.target as HTMLInputElement).value; }}
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' && inputVal.value.trim()) addTodo(); }}
/>
<button class="w-10 h-10 rounded-lg bg-blue-600 text-white font-bold cursor-pointer hover:bg-blue-500" onclick={addTodo}>+</button>
</div>
<FxReturn value={() => todos.value.length === 0 ? <p class="text-gray-500 text-center py-8 italic">No todos yet.</p> : null} deps={[todos.value.length]} />
<ul class="space-y-2">
<For items={todos.value} as="todo">
<li key={todo.id} class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-white/[0.03] group">
<FxReturn value={() => strikeTodo(todo)} deps={[todo.done]} />
<FxReturn value={() => <span class={`flex-1 text-sm ${todo.done ? 'text-gray-500 line-through' : 'text-gray-200'}`}>{todo.text}</span>} deps={[todo.done]} />
<button class="text-gray-300 hover:text-red-400 text-xs cursor-pointer opacity-0 group-hover:opacity-100 w-5 text-center" onclick={() => removeTodo(todo.id)}>✕</button>
</li>
</For>
</ul>
</div>
);
}📊 Comparison
| Feature | React | SolidJS | Svelte 5 | @rabbx/fx |
|---|---|---|---|---|
| Reactivity Model | VDOM diffing | Fine-grained signals | Runes (signals) | Fine-grained signals |
| Compilation | Runtime createElement | Compile-time transforms | Compile-time runes | Compile-time imperative DOM |
| List Rendering | .map() + keys | <For> | {#each} | <For> with O(n) keyed reconciliation |
| Conditional | Ternary / && | <Show> | {#if} | <Show>, &&, FxReturn |
| Directives | ❌ | ❌ | use: | ✅ use: with reactive params |
| Partial Hydration | Server Components | Islands (experimental) | Islands | ✅ Built-in islands |
| Runtime Size | ~40kb | ~7kb | ~2kb | ~3kb |
| SSR Strategy | Streaming | Async hydration | Async hydration | Replace-on-hydrate |
| Context | Provider tree traversal | useContext | Context API | Signal stacks (O(1)) |
| TypeScript | JSX types | Generics | Generics | Full inference, no JSX types needed |
🏗️ Architecture
┌─────────────────────────────────────────────────┐
│ User Writes JSX │
└──────────────────────┬──────────────────────────┘
│
┌─────────────▼──────────────┐
│ SWC (Strip TypeScript) │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ OXC Parser (TSX AST) │
└─────────────┬──────────────┘
│
┌──────────────────▼──────────────────────┐
│ MagicString Transform │
│ │
│ • Macros → imperative loops/effects │
│ • .map/.filter → keyed $effect │
│ • use: → action calls + onDestroy │
│ • && / ternary → conditional guards │
│ • Components → lifecycle wrappers │
│ • Imports → auto-injected │
└──────────────────┬──────────────────────┘
│
┌─────────────▼──────────────┐
│ Pure Imperative JS │
│ (_el, setAttr, append, │
│ on, $effect, reconcile) │
└────────────────────────────┘🤝 Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
💜 Support
If you find @rabbx/fx useful, consider supporting development:
📄 License
MIT © rabbxdev
Built with signals. Compiled to nothing.
