vanilla-way
v0.1.2
Published
A tiny signals-based reactive UI library with custom JSX.
Maintainers
Readme
vanilla-way
A tiny (~2.6 KB gzipped) React alternative: signals + custom JSX, real DOM, no virtual DOM.
Zero runtime dependencies. ESM-only. MIT licensed.
What it is
vanilla-way is positioned against React: instead of a virtual DOM and reconciliation, it uses signals and fine-grained reactivity. A signal change touches exactly the attribute or text node it is bound to — no re-render, no diff.
It is the framework that fits in your head: a small, readable core you can learn in one sitting. One mental model — signals push, derives recompute, owners clean up.
Install
npm i vanilla-wayEverything is imported from the package barrel:
import { h, createStore, createView } from "vanilla-way";What's included
- Signals +
computed— reactive state viacreateStore, derived values viacomputed/.derive - Custom JSX — the
hfactory andFragment, producing realHTMLElements (HTML + SVG) - Owner tree —
Owner,track,runWithOwner,createViewdrive all cleanup For— reactive lists, keyed (move/reuse) or unkeyed (full rebuild)Show— conditional rendering with optional fallbackcreateResource— async primitive withstatus/data/errorsignals andrefetchPortal— render children into a different DOM subtree (modals, tooltips)ErrorBoundary— catch render-time errors in a subtree and show a fallback (see scope below)
Mental model
- JSX returns real
HTMLElements — no virtual nodes, no reconciliation - Reactivity is fine-grained — a signal change touches exactly the binding it affects
- Owners form a tree that drives all cleanup (computeds, subscriptions, child views); disposal flows down it, errors bubble up it
Quickstart
import { h, createStore, createView } from "vanilla-way";
const store = createStore({ count: 0 });
const view = createView(() => {
const doubled = store.count.derive((n) => n * 2);
return (
<div class="counter">
<p>count: {store.count}</p>
<p>doubled: {doubled}</p>
<button onClick={() => store.count.set(store.count.get() + 1)}>
increment
</button>
<button onClick={() => store.count.set(0)}>reset</button>
</div>
);
});
document.body.appendChild(view.el);
// later, to tear down all subscriptions:
// view.dispose();JSX setup
vanilla-way uses the classic JSX transform (no Babel plugin, no Vite transform). Consumers using JSX must configure their tsconfig.json:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}ErrorBoundary scope
ErrorBoundary is Tier A: it catches synchronous render/setup-time throws in the owned subtree (a function component, reactive prop/child, For/Show builder, first computed run, or a synchronously-throwing createResource setup) and renders a fallback(err, reset) in their place. reset() retries.
It does not catch:
- DOM event-handler throws (e.g. an
onclickthat throws) — they fire outside the render call stack - Reactive update-time throws — a
computed/subscriber that throws on a later signal change surfaces at the.set()call site createResourceasync rejections — already exposed viaresource.error/resource.status === "error"
Guard event handlers and async work yourself. See docs/error-handling.md for the full model.
Non-goals
These are deliberately not on the roadmap:
- No virtual DOM
- No JSX compiler / Babel plugin (classic
h()only) - No SSR / hydration
- No built-in router
- No devtools
- No automatic batching or scheduling
If any of these matter for your project, use React.
Status
- Version
0.1.0— experimental 0.x; the API may still evolve - Zero runtime dependencies, ESM-only, MIT licensed
- Tests live in
tests/— runnpm testfrom this directory
