DocsBuilding pages
PulsePoint runtime
The browser half of Rahti: it mounts the boundaries the server named, evaluates their scripts, tracks hook state, applies bindings, and handles SPA navigation and RPC calls. It ships prebuilt — there is no JavaScript build step.
A file, not a build step
The runtime is a prebuilt asset. An application ships public/js/pp-reactive-v2.min.js and mounts it from public/js/main.js. cargo rahti new writes the same bundle into every project it creates, and cargo rahti upgrade replaces it when the framework ships a new build.
The authoring model
PulsePoint scripts are plain JavaScript written inside the reactive root they belong to. The root and the script are one component scope.
html! {
<section>
<p>{count}</p>
<button onclick={setCount(value => value + 1)}>"Increment"</button>
<script>
const [count, setCount] = pp.state(0);
</script>
</section>
}Hooks must run in a stable order during every render, as in React. Do not call them conditionally, or after the component is disposed.
Component boundaries
The runtime reads the served document back and mounts one scope per boundary. A boundary arrives in one of three shapes, all written by html! and never by hand:
- An element carrying
pp-component— the ordinary case: a page's root, a component's root, a single-rooted child fragment. The element anchors the scope. - A template — around a root layout's
<slot />, and around server-deferred content generally. The runtime materialises it into live DOM before mounting. - A comment pair — around a run of siblings from a
<>…</>fragment. At mount the runtime raises the pair into a live, layout-invisible boundary element.
Two things are deliberately not boundaries. A block whose root is a component tag contributes no markup of its own, so it adds no scope. And several instances of one component are separate scopes even though they share a name — the runtime derives a unique identity per instance, which is why two copies of the same counter count independently.
The binding surface
| Syntax | Purpose |
|---|---|
| {expression} in text | Escaped reactive text or value. |
| {expression} in a quoted attribute | A reactive attribute or property. |
| any native on* attribute | An event handler expression. |
| pp-for="item in items" | A loop on a <template>. |
| pp-for="(item, index) in items" | The same, with an index. |
| key="{expression}" | Repeated-row identity. |
| pp-ref={ref} | Binds a native element to a ref. |
| pp-style="{cssText}" | Dynamic inline style. |
| pp-spread="{object}" | Spreads dynamic attributes. |
| <token.provider value="{value}"> | A context provider. |
| pp-spa="false" on an anchor | Opts out of SPA interception. |
| pp-reset-scroll="true" | Resets a scroll container on navigation. |
| pp-scroll-key="name" | A stable scroll-restoration identity. |
| pp-loading-content="true" | The region a loading.rs replaces. |
| pp-loading-transition | Fade timing for that swap, as JSON. |
Value rendering
A binding does not print its value with String(...). PulsePoint serializes it the way React serializes a JSX child. Read this before binding a value that is not a plain string or number — most surprises reported as "my binding is blank" are one of these rules working as designed.
In text position
| Value | Rendered |
|---|---|
| "text", 42, 12n | The value, HTML-escaped. |
| true / false | Nothing. Both booleans, not only false. |
| null / undefined | Nothing. |
| "" | Nothing — it is the empty string, not a blank. |
| 0 | 0. Falsy, but printed. |
| an array | Each item by these same rules, concatenated with no separator. |
| an object, function or symbol | Nothing, plus a [PP-WARN] console line. React throws here; PulsePoint omits and warns. |
The boolean row is the one that costs time. A value seeded from Rust as false, or a None seeded as null, arrives in the browser correctly and then renders as an empty span.
// admin === false renders nothing at all — both booleans vanish.
<p>"Admin: "<span>{admin}</span></p>
// Spell the display out in the expression instead.
<p>"Admin: "<span>{admin ? "yes" : "no"}</span></p>
<p>"Role: "<span>{role ? role : "none"}</span></p>
// The React `&&` idiom leaks a zero: `0` prints, only booleans and nullish
// values vanish.
<p>{items.length > 0 ? "Some" : "None"}</p>In attribute position
| Attribute kind | Value | Result |
|---|---|---|
| HTML boolean (disabled, hidden, checked…) | true | Attribute present. |
false, nullish, or a falsy primitive | Attribute absent. | |
| a truthy primitive | Present, carrying that value. | |
| anything else (class, title, aria-*, data-*) | true / false | The literal strings "true" / "false". |
null / undefined | Attribute present, with an empty value. | |
0, NaN | "0", "NaN". |
Two of those rows differ from React. A boolean on a non-boolean attribute is serialized rather than dropped, because aria-pressed and data- flags are string-valued in the DOM. And a nullish value leaves the attribute present and empty — harmless on class or title, not harmless on a URL, where src="" re-requests the current document.
// A nullish value leaves a non-boolean attribute present and empty, and
// src="" re-requests the current document. Guard URL attributes.
<img src="{avatar ? avatar : placeholder}" />A pp-for collection that is not iterable renders no rows and logs a warning; null and undefined render no rows silently. Binding an object rather than an array is a blank list, not an error.
Events
onclick={save()}
oninput={setName(target.value)}
// Inside an event expression the runtime exposes `event`, `e`, `$event`,
// `target`, `currentTarget` and `el`. Use native lowercase names — `onclick`,
// never `onClick`.Forms
value={state} and checked={state} are controlled bindings; default values are uncontrolled. Do not switch a control between the two after mount. PulsePoint preserves selection and focus, and handles input, textarea, select, checkbox, radio and form-reset behaviour.
// Controlled: the binding owns the value.
<input value={name} oninput={setName(target.value)} />
<input type="checkbox" checked={agreed} onchange={setAgreed(target.checked)} />
// A file input is imperative — reach it with a ref.
<input type="file" pp-ref={file} />
<script>
const file = pp.ref(null);
</script>Lists
For browser-owned collections, loop over a template. The collection must be iterable, and stable keys preserve row identity and local state. For a server-owned list rendered once in Rust, use Html::concat instead.
<ul>
<template pp-for="(item, index) in items">
<li key="{item.id}">{index}: {item.label}</li>
</template>
</ul>Hooks
| Hook | What it gives you |
|---|---|
| pp.state(initial) | [value, setter]. The initial value may be lazy; the setter takes a replacement or (previous) => next. Equal values use Object.is and do not rerender. |
| pp.effect(fn, deps?) | Runs after commit; may return a synchronous cleanup. Start async work inside — do not return a Promise. [] runs once per mount. |
| pp.layoutEffect(fn, deps?) | Runs synchronously after DOM mutation, before normal effects. For measurement. |
| pp.ref(initial?) | A stable { current } object, for pp-ref, portals and imperative handles. |
| pp.memo(factory, deps?) | Memoises a computed value. |
| pp.callback(fn, deps?) | Memoises a function identity, for stable subscriptions. |
| pp.reducer(reducer, initial, init?) | [state, dispatch], with an optional initialiser. |
| pp.createContext(default) / pp.context(token) | Logical ancestor context. Read it in the render phase, not inside a later callback. |
| pp.portal(ref, target?) | Moves content elsewhere in the DOM while preserving logical ancestry, context and lifecycle. |
| pp.id() | A stable component-local identifier for id, for and ARIA pairs. |
| pp.errorBoundary() | [error, reset] for descendant render and effect failures. A client boundary, distinct from error.rs. |
| pp.syncExternalStore(subscribe, getSnapshot) | Subscribes to browser or external state. |
| pp.imperativeHandle(ref, create, deps?) | Publishes a controlled imperative API. |
| pp.transition() | [isPending, startTransition]. Pending until the scope or returned Promise settles. |
| pp.deferredValue(value, initial?) | A value that updates after the current commit. |
| pp.optimistic(passthrough, reducer?) | [optimisticValue, addOptimistic]. Pending actions replay over the confirmed value. |
| pp.props | Boundary props resolved from root attributes. A Rust function argument does not automatically appear here. |
pp.effect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id); // synchronous cleanup only
}, []);const [pending, startTransition] = pp.transition();
async function save() {
await startTransition(async () => {
const saved = await pp.rpc("save_note", { text });
setNote(saved);
});
}// A provider is the lowercase token-derived `.provider` tag.
<theme.provider value="{scheme}">
<slot />
</theme.provider>
<script>
const theme = pp.createContext("light");
const scheme = pp.context(theme);
</script>Runtime utilities
| Call | What it does |
|---|---|
| pp.mount() | Mounts the document. public/js/main.js calls it. |
| pp.redirect(url) | A PulsePoint-aware navigation. |
| pp.rpc(name, data?, optionsOrAbort?) | Calls a Rahti #[rpc]. See RPC & uploads. |
| pp.socket(name, args?, handlers?) | Opens a WebSocket to a #[socket] function. |
| pp.enablePerf() / pp.disablePerf() | Toggle runtime performance sampling. |
| pp.getPerfStats() / pp.resetPerfStats() | Per-component render-phase aggregates. |
twMerge is published as a global by public/js/main.js, for when a reactive class string can contain conflicting Tailwind utilities.
// `twMerge` is published as a global by public/js/main.js.
<div class={twMerge("p-2 px-4")}></div>SPA navigation
After mount, PulsePoint intercepts eligible same-origin links, fetches the next document, morphs owned DOM, preserves component identity where it can, and manages focus, scroll, history, redirects and loading regions.
// Opt one link out of SPA interception — an external site, a download, a
// route that must reload the document.
<a href="/report.pdf" pp-spa="false">"Download"</a>Crossing root layouts
A body swap only makes sense while the document stays the same. When an application has more than one root layout, the two shells have different heads — different stylesheets, different scripts — and swapping one body into the other would leave the page running the wrong shell.
So PulsePoint compares the X-PP-Root-Layout header of the navigation response with the <meta name="pp-root-layout"> of the current document. When both are present and they differ, it abandons the SPA path and performs an ordinary full load. Equal values navigate in place as usual, and Rahti sends both halves for every page a root layout wraps.
Loading regions
While a navigation is in flight the runtime replaces the first element marked pp-loading-content="true" — or the whole <body> when nothing is marked — with the nearest loading region, then restores it from the response. Ownership, the literal-path walk and the transition JSON are all in Routing.
APIs that are not there
Do not invent React compatibility. PulsePoint v2 has no direct equivalent for forwardRef, component-wrapper memo, lazy, Suspense, useInsertionEffect, useActionState, useFormStatus, or a free-standing startTransition.
