@papack/csr
v1.0.2
Published
CSR UI Dom runtime
Readme
@papack/csr
Experimental synchronous UI DOM runtime. Designed for simplicity, explicitness, and predictability.
No feature is the feature.
No Virtual DOM. No diffing. No scheduler. No hidden async.
Just real DOM, real mutation, and deterministic structure with zero dependencies.
Philosophy
This library is simple by design, not “easy”.
- There is one way to do things
- Everything is explicit
It is optimized for clarity and long-term maintenance.
Features
- Fully synchronous rendering
- No Virtual DOM, no reconciliation
- Minimal reactivity via
signal/effect - Explicit lifecycle (
mount,unmount) - Structural primitives (
Show,For)
Core Ideas
Signals are active state
Signals are sources, not values.
const [count, setCount] = signal(0);
setCount((v) => v + 1);
setCount(() => 42);- no equality checks
- no dependency tracking
- mutation is allowed
- every write notifies every subscriber
Effects are explicit reactions to signals
effect(count, (value) => {
console.log(value);
});Characteristics:
- NOT runs immediately on mount
- only runs on every write to the signal
- only one singal, no dependency arrays
- async callbacks are allowed (fire-and-forget)
Effects are not awaited. Concurrency is explicit and intentional.
Lifecycle is bound to real DOM
Lifecycle is structural, not conceptual.
mount((parent) => {
// runs once, when the root DOM element exists
});
unmount(() => {
// guaranteed cleanup before removal
});- lifecycle is attached to actual DOM nodes
- children unmount before parents
- removal means real
destroy
Example
import { render, signal, for } from "@papack/csr";
const [items, setItems] = signal([
{ uuid: "a", name: "A" },
{ uuid: "b", name: "B" },
]);
render(<App />, { parent: document.body });
function App() {
return (
<ul>
<For each={items}>{(item) => <li>{item.name}</li>}</For>
</ul>
);
}Structural Rendering
Show
Controls existence, not visibility.
<Show when={visible}>
<User />
</Show>- when
false, the subtree is destroyed - when
true, it is rendered fresh - lifecycle runs correctly on both transitions
For (intentionally restricted)
For is a keyed structural renderer, not a generic iterator.
Rules:
eachmust be an array- each item must be an object
- each item must have a stable key (
uuid) - no fallbacks, no heuristics
type Item = {
uuid: string;
[key: string]: any;
};What For does:
detects:
- additions
- removals
- order changes
performs:
- DOM moves (
insertBefore) - rendering only for new keys
destroy()for removed keys
- DOM moves (
What For does not do:
- no content diffing
- no re-rendering existing items
- no prop patching
If an item’s content changes, the item itself must be reactive.
Mutation is allowed
setItems((prev) => {
prev.push({ uuid: "c", name: "C" });
return prev;
});This is correct.
Why:
- signals are active
- updates are not reference-based
Foronly cares about keys and order
Routing
Routing is just application state.
const [route, setRoute] = signal("home");Structure is derived explicitly:
effect(route, (r) => {
setIsHome(() => r === "home");
setIsSettings(() => r === "settings");
});<Show when={isHome}>
<Home />
</Show>
<Show when={isSettings}>
<Settings />
</Show>