Page-level reactivity
Markup that comes alive in the browser without a component behind it is page-level reactivity. The unit is a facet: a signal or a handler bound to a single node of the page’s markup. A signal written as a child or as a prop, or a handler the browser rebuilds to write back to one, is all it takes—the machinery an island uses, applied to a single node.
The same machinery as an island
An island is a marked DOM range, an index into a payload, and a Preact root rendered over that range. A facet is those three pieces with a smaller unit: one text node, or one element with its props and handlers. What you skip is declaring a component, and shipping a client module for it.
hum:signalbrackets a text node,data-hum-signalnames the props to rebind, anddata-hum-handlerthe handlers. All three carry payload indexes, exactly likehum:island, and the payload is embedded only when the page has something to revive.- Hydration is scheduled for both, so nothing is live until the task after
bootreturns. - What a facet cannot do is hold state. An island owns a component instance, which survives its region being re-rendered, while a page-level signal is revived from the payload of each response. Component state, hooks, or a value that has to stay live on the client are still island work.
Interactive runtime model
The client-side InteractionManager coordinates DOM ownership, mounting, scheduling and cleanup for islands, partials and bindings. Islands and partials host their nested bindings; a signal outside a host gets a standalone binding range. Each response carries one shared payload graph, so signals keep their identity across these units. Partial navigation routes updates through the same manager.
Bind a signal
As a child it renders its value on the server and stays a live text node; as a prop the matching DOM property follows the signal afterwards. Booleans work the same way, so a checkbox or a details element reflects its signal by itself.
import { signal } from "@preact/signals";
import { add, setFromProp } from "@humjs/core/client";
export default function Checkout() {
const tickets = signal(2);
return (
<form>
<button type="button" onClick={add(tickets, -1, { min: 1, max: 8 })}>−</button>
<output>{tickets}</output>
<button type="button" onClick={add(tickets, 1, { min: 1, max: 8 })}>+</button>
<input
type="number"
value={tickets}
onInput={setFromProp(tickets, "value")}
/>
</form>
);
}One tickets signal is behind the text, the field and both buttons, so a change from any of them updates the others. Creating it with the view is on purpose: the server renders its value into that response—so it may come from the request—and the client revives its own copy. Inside an island, where renders repeat, use useSignal instead.
Handlers come from factories
A handler is rebuilt in the browser, so it travels as a factory ID plus the arguments it was called with—a closure cannot be serialized and throws at render time. @humjs/core/client ships add(signal, delta, options), setValue, toggle and setFromProp(signal, key); a handler of your own is a factory too.
// routes/checkout.tsx — alongside the page and server handlers
import { defineFactory } from "@humjs/core/client";
import type { Signal } from "@preact/signals";
/** Resets both options at once; each built-in factory drives one signal. */
const resetOrder = defineFactory(
(
tickets: Signal<number>,
giftWrap: Signal<boolean>,
defaults: { tickets: number; giftWrap: boolean },
) =>
() => {
tickets.value = defaults.tickets;
giftWrap.value = defaults.giftWrap;
},
);Hum generates the factory ID from its file path and variable name. Declare it at module scope; exporting is optional. Hum extracts its browser implementation and loads it before restoring handlers. It can share a route file with server handlers, and needs no client-entry import. Pass request-specific values and signals as factory arguments.
Try it
A checkout made of facets
Every control below is a facet on this page’s own markup. The quantity and the gift-wrap flag are signals; the controls call handlers that the browser rebuilt from the payload, including the custom factory behind Reset.
Previous: Partial page updates →