Rendering Architecture (Advanced)

Retend separates reactive logic, components, and state management from platform-specific rendering. The core library (retend) delegates interface output to a Renderer.

Different renderers support web applications (retend-web) and static HTML generation (retend-server). A renderer can also target another platform.

How Renderers Work

When Retend processes JSX and reactive Cells, it determines the required changes and calls methods on the active renderer to apply them.

  1. Decoupled Types: The core framework does not need to know the concrete type of a Node. It treats a node as an opaque object. In retend-web, a node is an HTMLElement; in retend-server, it is a virtual DOM object such as VNode, VElement, or VText that implements the relevant browser DOM API.
  2. Global Context: Retend stores the active renderer in a global context. renderToDOM from retend-web sets the browser renderer as active before processing the application.
  3. Reconciliation & Updates: The core framework handles fine-grained reactivity and list reconciliation, then sends the renderer commands such as updating a node's text or appending a child.

Built-in Renderers

Retend comes with two built-in renderer implementations:

DOMRenderer (retend-web)

The standard renderer for browser environments:

import { renderToDOM } from 'retend-web';

renderToDOM(document.getElementById('app'), App);

Key characteristics:

  • Works with real DOM HTMLElement nodes
  • Supports hydration for SSR
  • Handles event listeners, styles, and attributes
  • Full support for all web APIs (shadow DOM, teleport, etc.)

VDOMRenderer (retend-server)

A virtual DOM renderer for server-side rendering:

import { renderToString } from 'retend-server/client';
import { VDOMRenderer, VWindow } from 'retend-server/v-dom';

const window = new VWindow();
const renderer = new VDOMRenderer(window);
const nodes = renderer.render(<App />);
const html = renderToString(nodes, window);

Key characteristics:

  • Uses lightweight virtual nodes (VNode, VElement, VText)
  • No browser APIs required
  • Generates static HTML strings via renderToString()
  • Supports marking dynamic nodes for hydration

Custom Renderers

Implement the Renderer interface to target another platform. The experimental packages include these examples:

  • CanvasRenderer: Renders to HTML5 Canvas
  • TerminalRenderer: Renders to terminal UI

Renderer Capabilities

Renderers declare their feature support via the capabilities property:

interface Capabilities {
  supportsSetupEffects?: boolean; // Can run setup effects
  supportsConnectedCallbacks?: boolean; // Supports onConnected
}

The framework uses these flags to:

  • Skip incompatible code paths
  • Provide fallbacks for limited environments
  • Optimize for specific platforms

The Renderer API

Every renderer in the Retend ecosystem implements the Renderer interface. A custom renderer for HTML5 Canvas or a command-line terminal UI must implement this interface.

Renderer Types

Every renderer defines concrete types for its specific platform via RendererTypes:

interface RendererTypes {
  Node: unknown; // The fundamental unit of output (platform-specific)
  Text: unknown; // A text node (platform-specific)
  Container: unknown; // A node that can hold other nodes (platform-specific)
  Group: unknown; // A logical container for fragments (platform-specific)
  Handle: unknown; // Reference for dynamic lists (platform-specific)
  Host: EventTarget; // The target environment
}

Complete API Reference

MethodDescriptionParametersReturns
render(app)Renders a JSX templateapp: JSX.TemplateNode | Node[]
createContainer(tagname, props?)Creates a host elementtagname: string, props?: anyContainer
createText(text, isReactive?, isPending?)Creates a text nodetext: string, isReactive?: boolean, isPending?: booleanNode
updateText(text, node)Updates an existing text nodetext: string, node: TextNode
createGroup()Creates a logical group for fragments-Group
unwrapGroup(fragment)Flattens a group to nodesfragment: GroupNode[]
createGroupHandle(group)Creates stable handle for dynamic listsgroup: GroupHandle
getHandleNodes(handle)Returns nodes associated with a handlehandle: HandleNode[]
write(handle, newContent)Replaces content in a handlehandle: Handle, newContent: Node[]void
reconcile(handle, options)Efficiently updates a dynamic listhandle: Handle, options: ReconcilerOptionsvoid
append(container, children)Attaches children to parentparent: Node, children: Node | Node[]Node
setProperty(container, key, value)Sets a property/attributenode: N, key: string, value: unknownN
isNode(child)Checks if value is a valid nodechild: anyboolean
isGroup(child)Checks if value is a groupchild: anyboolean
isActive(node)Checks if node is activenode: Nodeboolean
handleComponent(fn, props, snapshot?, fileData?)Executes a componentfn: Function, props: any[], snapshot?: StateSnapshot, fileData?: JSX.JSXDevFileDataNode | Node[]

Node Creation

The core framework calls these methods when it needs to create new elements:

  • createContainer(tagname, props): Creates a host-level entity (like a <div>).
  • createText(text, isReactive?, isPending?): Creates a text node. The isReactive flag indicates if the text content may change.
  • createGroup(): Creates a logical grouping of nodes without a physical container (used for <></> Fragments).

Node Updates

When a reactive Cell changes, the core framework tells the renderer exactly what to do:

  • updateText(text, node): Mutates the text of an existing text node.
  • setProperty(node, key, value): Applies a property or attribute to a node.
  • append(parent, children): Physically attaches nodes to a parent.

Dynamic Collections

When a For() loop needs to update a list of items, the core framework handles the diffing algorithm and then commands the renderer:

  • createGroupHandle(group): Creates a stable reference to track a dynamic section.
  • write(handle, newContent): Replaces all content between handle markers.
  • reconcile(handle, options): Efficiently creates, moves, or removes nodes to match a new list.

List Reconciliation

The reconcile method implements Retend's efficient list diffing. The ReconcilerOptions interface:

interface ReconcilerOptions<Node> {
  retrieveOrSetItemKey: (item: any, i: number) => any;
  onBeforeNodeRemove?: (node: Node, fromIndex: number) => void;
  onBeforeNodesMove?: (nodes: Node[]) => void;
  cacheFromLastRun: Map<any, ForCachedData<Node>>;
  newCache: Map<any, ForCachedData<Node>>;
  newList: Iterable<any>;
  nodeLookAhead: Map<unknown, { itemKey: any; lastItemLastNode: Node | null }>;
}

interface ForCachedData<Node> {
  index: Cell<number>;
  nodes: Node[];
  snapshot: StateSnapshot;
}

The core framework handles the diffing algorithm and calls the renderer's reconcile method with the required changes.


This separation keeps platform-specific behavior in renderers while the core handles reactive updates.