Skip to content

Function: renderSpec()

API / @xmachines/play-dom / renderSpec

function renderSpec(
spec,
store,
registry,
send,
handlers,
fallback?,
onRenderError?,
functions?,
loading?,
onWatchSetup?,
validationFunctions?,
navigate?,
): Node | null;

Defined in: packages/play-dom/src/json-render/renderer.ts:135

Render a Spec tree into DOM nodes using the provided DomRegistry.

Traverses the spec from spec.root, evaluates visibility, resolves all prop expressions (including $state, $bindState, $computed, $template, etc.), and calls the matching DomComponentRenderer for each visible element.

Event wiring and child rendering are handled inside the component wrapper built by defineRegistry — this function only orchestrates the traversal.

Parameters

ParameterTypeDescription
specSpecThe json-render Spec describing the UI tree.
storeStateStoreLive StateStore bound to spec.state.
registryDomRegistryMap of element type names → DomComponentRenderer.
send(event) => voidDispatcher for interaction events (e.g. actor.send).
handlersRecord<string, ActionHandler>Map of catalog action names → async ActionHandler functions.
fallback?DomComponentRendererOptional renderer called when registry has no entry for an element’s type (GAP-04). If absent, unknown types return null.
onRenderError?RenderErrorHandlerOptional callback matching RenderErrorHandler = (error, name). Invoked for three distinct error classes: - (error, elementType) when a component renderer throws (GAP-08) - (error, actionName) when an action handler rejects during emit() - (error, actionName) when an action handler rejects in a watch binding If absent, all three fall back to console.error. Forwarded into DomRenderContext.onRenderError so emit() and watch handlers route errors through the same channel as component render errors.
functions?Record<string, ComputedFunction>Optional map of named compute functions forwarded to PropResolutionContext (GAP-03). Enables { $computed: "fn", args } prop expressions to resolve. When absent, $computed expressions return undefined (backward-compatible, no throw).
loading?booleanOptional flag indicating the spec is still streaming (GAP-06). When true, components can read ctx.loading to render skeleton states, and missing-child warnings (GAP-07) are suppressed since referenced elements may not yet have arrived in the incremental spec.
onWatchSetup?(cleanup) => voidOptional callback invoked with each watch subscription cleanup function (GAP-02). When an element declares watch, the renderer sets up a store subscription and passes its unsubscribe function to this callback. Callers (e.g. PlayRenderer) collect these to call on disconnect().
validationFunctions?Record<string, (value, args?) => boolean>Optional map of custom validation functions forwarded to DomRenderContext.validationFunctions. Components pass these as customFunctions to runValidationCheck / runValidation from @json-render/core. Has no effect on prop resolution.
navigate?(path) => voidOptional navigation callback forwarded to DomRenderContext.navigate. Invoked automatically by defineRegistry’s emit() when an action binding resolves with onSuccess: { navigate: "/path" }. Also readable by component implementations via ctx.navigate.

Returns

Node | null

The root DOM Node, or null if the root is invisible or has no registered renderer.

Remarks

Devtools isolation in tests: The module-level devtoolsActive flag is a singleton that persists for the lifetime of the module. In test environments, any call to markDevtoolsActive() MUST be paired with a release() call inside a try/finally block. Failing to do so leaves devtoolsActive = true for all subsequent tests, causing false positives in devtools-conditional rendering paths (e.g. data-jr-key wrapper spans).

const release = markDevtoolsActive();
try {
// test code
} finally {
release();
}

Example

const { registry, handlers } = defineRegistry(catalog, { components, actions });
const resolvedHandlers = handlers(
() => undefined,
() => ({}),
);
const node = renderSpec(spec, store, registry, actor.send.bind(actor), resolvedHandlers);
if (node) container.appendChild(node);