Skip to content

API reference

Everything kerf exports, organised by module. Imported via import { … } from 'kerfjs' unless noted.

A reactive value. .value reads / writes; reads inside effect() / computed() are tracked.

computed<T>(fn: () => T): ReadonlySignal<T>

Section titled “computed<T>(fn: () => T): ReadonlySignal<T>”

A derived signal. Re-runs fn whenever any signal it reads changes. Read-only.

Run fn immediately, then re-run it whenever any signal it reads changes. Returns a disposer.

Run fn, deferring effect re-runs until fn returns. Multiple writes inside fn produce a single re-run.

interface Signal<T> { value: T }
interface ReadonlySignal<T> { readonly value: T }

defineStore<TState, TActions>(spec): Store<TState, TActions>

Section titled “defineStore<TState, TActions>(spec): Store<TState, TActions>”
defineStore({
initial: () => TState,
actions: (set: (next: TState) => void, get: () => TState) => TActions,
});

Creates a store with state: ReadonlySignal<TState>, actions: TActions, reset(): void. Registers in the global registry consumed by resetAllStores().

Calls reset() on every store registered via defineStore().

interface Store<TState, TActions> {
readonly state: ReadonlySignal<TState>;
readonly actions: TActions;
reset(): void;
}

clearStoreRegistry(): voidkerfjs/testing subpath

Section titled “clearStoreRegistry(): void — kerfjs/testing subpath”

Empties the global store registry. Used by unit tests to isolate cases. Imported via the kerfjs/testing subpath, not the main kerfjs entry, so production builds don’t pull it in:

import { clearStoreRegistry } from 'kerfjs/testing';

mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void

Section titled “mount(rootEl: HTMLElement, render: () => SafeHtml | string): () => void”

Bind render() to rootEl’s children. Wraps effect() with kerf’s segment-aware diff. Returns a disposer.

The diff:

  • Only ever touches rootEl’s subtree; rootEl itself is preserved.
  • Matches elements by id, then data-key. Position otherwise.
  • Short-circuits on the live element when:
    • It has data-morph-skip (subtree preserved as-is).
    • It’s a list parent owned by each(...) (children-only short-circuit; each’s reconciler owns those rows). Attribute morphing on the parent itself still happens.
    • fromEl.isEqualNode(toEl) (no work needed).
    • It’s the focused [contenteditable] (entire subtree preserved on this morph; see §8.7 below and docs/4-render.md §4.4).
  • Otherwise preserves the focused text-entry’s value + selection range, then proceeds.

Lists rendered with each(...) go through a separate keyed reconciler that operates directly on the live parent’s children — O(changes), not O(rows). See each below.

each(rows.value, (row) => <tr data-key={row.id}>{row.label}</tr>);
each(rows.value, (row) => <tr…>…</tr>, (row) => row.id === selectedId ? 1 : 0);

Keyed list iteration with per-item memoisation, routed through mount()’s native list reconciler. Skips re-running render for items whose object identity (and optional key) are unchanged since the previous call — those items keep their existing live DOM nodes verbatim. Items whose identity or key did change get a fresh node (all fresh-node HTML for a render is bulk-parsed in one innerHTML call); items that disappeared are removed. Reorders use a longest-increasing-subsequence pass so the number of insertBefore calls is the minimum possible. Items must be objects (cache is a WeakMap); wrap primitives if you need to iterate them. Each item’s render output must produce exactly one top-level element. Use key when external state, not the item itself, drives what the row should render (e.g. a “currently selected” id flips a CSS class).

If a descendant of a moved row holds focus, the reconciler snapshots the active element + its selection range before the move pass and re-applies them afterwards — so focus and caret position survive a reorder even on engines that drop focus on insertBefore (older Safari, happy-dom). See docs/4-render.md §4.4.

delegate(rootEl, type, selector, handler): () => void

Section titled “delegate(rootEl, type, selector, handler): () => void”
delegate(rootEl, 'click', '[data-action="add"]', (event, matched) => { ... });
delegate(rootEl, 'focus', '.field-row', (event, row) => { ... });

One root listener with closest(selector)-style walk-up matching; fires handler(event, matched) if the match is inside rootEl. Returns a disposer.

Auto-promotes the well-known non-bubbling event types (focus, blur, scroll, load, error, mouseenter, mouseleave) to capture phase under the hood, so the call site looks identical regardless of whether the event bubbles. Selector matching stays closest()-style for every event type — wrapper selectors still match when the event lands on a descendant.

delegateCapture(rootEl, type, selector, handler): () => void

Section titled “delegateCapture(rootEl, type, selector, handler): () => void”

Same shape, but installs on the capture phase and matches via target.matches(selector) (direct match, no walk-up). The escape hatch — use it for custom non-bubbling events that aren’t in delegate()’s auto-promotion list, or when you want capture-phase semantics with strict element-match behaviour.

import 'kerfjs/jsx-runtime' — TypeScript / esbuild config

Section titled “import 'kerfjs/jsx-runtime' — TypeScript / esbuild config”
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "kerfjs"
}
}
class SafeHtml {
readonly __html: string;
constructor(html: string);
toString(): string;
}

The return type of every JSX expression. .toString() returns the underlying HTML.

SafeHtml instances carry a brand symbol — Symbol.for('kerfjs.SafeHtml') — so cross-bundle identification works even if a consumer’s bundler ends up loading two copies of kerf (e.g. the barrel and the JSX-runtime entry resolved as independent modules). Prefer isSafeHtml() over instanceof SafeHtml when writing custom integrations.

isSafeHtml(value: unknown): value is SafeHtml

Section titled “isSafeHtml(value: unknown): value is SafeHtml”

Cross-bundle-safe type guard. Returns true for any object carrying the Symbol.for('kerfjs.SafeHtml') brand. Use this rather than instanceof SafeHtml if you’re inspecting JSX values yourself — instanceof fails when two copies of kerf produce structurally-identical-but-class-distinct SafeHtml instances.

Wrap a pre-escaped HTML string. Useful for icons, rendered Markdown, server-included fragments.

JSX <>...</> — concatenates children without a wrapper tag. Available from both kerfjs/jsx-runtime (used by the JSX transform) and the main kerfjs barrel (when you need to compose Fragment manually, e.g. jsx(Fragment, { children })).

toElement(jsx: SafeHtml | string): Element

Section titled “toElement(jsx: SafeHtml | string): Element”

Parses a JSX/SafeHtml/string and returns a single DOM element. Detects SVG content (root <svg> or orphan SVG fragment) and routes through DOMParser('image/svg+xml') for correct namespacing. HTML takes the <template>.innerHTML path.

Throws if the input produces zero elements OR if DOMParser returns a parsererror.

AttributeEffect
id="..."Used as a diff key. Highest priority.
data-key="..."Used as a diff key. Lower priority than id.
data-morph-skip (any value, even empty)Subtree preserved as-is on every re-render.
Element kindBehaviour when focused during a morph
<input type="text" | "search" | "url" | "email" | "tel" | "password" | "">Live .value + selectionStart/selectionEnd copied to the morph target; morph proceeds (attribute updates apply).
<textarea>Same as text-entry inputs.
[contenteditable]Entire subtree skipped on this morph (same mechanism as data-morph-skip). User’s edit + caret + multi-range selection preserved verbatim; attribute updates deferred until the next render after blur. See docs/4-render.md §4.4.
Anything else (<button>, <a>, <div tabindex>, non-text inputs…)Morph proceeds normally — no special handling.