Custom Renderers

Build rEFui renderers for novel platforms: map your platform’s primitives to the small nodeOps interface and let signals drive updates. Prefer reusing a DOM shim (undom-ng) when possible; write a renderer only when you need platform-specific behavior. If your platform already exposes a DOM-like API, you can often reuse createDOMRenderer with a custom doc.

When to write one

If you already have a DOM-like API

Minimal interface (nodeOps)

Implement these methods and pass them to createRenderer(nodeOps):

setProps receives host props with the renderer metadata keys $ref and children omitted. When neither metadata key is present, it may receive the caller's original props object. Make it idempotent and do not mutate that object. Normalize events, styles, and platform-specific attributes here. Clean up in removeNode when needed. When a prop value is a signal, subscribe and update the native prop/handler on change (see DOM/HTML renderers).

getParent is optional. Without it, the renderer core keeps parent entries and removes an already-parented node before calling the host append/insert method. If it is implemented, return the node's current physical host parent (or null when detached); the core then tracks only logical fragment ownership. In that mode, appendNode and insertBefore must move already-parented ordinary nodes, matching DOM behavior.

clearChildren is an optional performance capability, not a required tree operation. Return false without changing the tree when the parent contains anything before first or after last. When it returns true, the renderer core restores the fragment anchors; the caller can then dispose the former children normally. The DOM renderer implements this with textContent = ''; tree renderers can clear their child collection. Renderers that omit it automatically keep the individual-removal fallback.

Rendering flow

  1. const R = createRenderer(nodeOps)
  2. R.render(root, App) or use JSX (classic: jsxFactory: 'R.c', jsxFragment: 'R.f'; automatic: jsxImportSource: 'refui').
  3. Signals drive retained updates; only touched nodes call setProps/append/remove.

Fragments

If the platform lacks fragments, return a lightweight child container from createFragment. The renderer core recognizes and expands fragments it creates; the host appendNode/insertBefore operations only need to move or flatten the raw fragment container they receive.

Props and events

Suggested structure

import { createRenderer, isSignal, watch } from 'refui'

const nodeOps = {
	isNode: (n) => !!n && n.type === 'node',
	createNode: (tag) => platformCreate(tag),
	createTextNode(text) {
		// mirror DOM/HTML behavior: track signal text with an owned effect
		if (isSignal(text)) {
			const n = platformCreateText('')
			watch(() => platformSetText(n, String(text.get() ?? '')))
			return n
		}
		return platformCreateText(String(text ?? ''))
	},
	createAnchor: () => platformCreateComment(''),
	createFragment: () => platformCreateFragment(),
	getParent: (node) => platformParent(node), // optional
	removeNode: platformRemove,
	clearChildren(parent, first, last) {
		if (!platformOwnsCompleteRange(parent, first, last)) return false
		platformClearChildren(parent)
		return true
	},
	appendNode(parent, ...kids) { kids.forEach(k => platformAppend(parent, k)) },
	insertBefore(node, ref) { platformInsertBefore(node, ref) },
	setProps(node, props) { platformSetProps(node, props) }
}

export const R = createRenderer(nodeOps)

Prefer shims when available

Debugging tips

Custom Render Targets

If you have a DOM-like API available on your target platform, you can often pass its doc implementation to createDOMRenderer instead of building a full nodeOps implementation from scratch. This allows you to leverage the existing DOM renderer logic for reactive properties and text.