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.
- Decoupled Types: The core framework does not need to know the concrete type of a
Node. It treats a node as an opaque object. Inretend-web, a node is anHTMLElement; inretend-server, it is a virtual DOM object such asVNode,VElement, orVTextthat implements the relevant browser DOM API. - Global Context: Retend stores the active renderer in a global context.
renderToDOMfromretend-websets the browser renderer as active before processing the application. - 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
HTMLElementnodes - 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
| Method | Description | Parameters | Returns |
|---|---|---|---|
render(app) | Renders a JSX template | app: JSX.Template | Node | Node[] |
createContainer(tagname, props?) | Creates a host element | tagname: string, props?: any | Container |
createText(text, isReactive?, isPending?) | Creates a text node | text: string, isReactive?: boolean, isPending?: boolean | Node |
updateText(text, node) | Updates an existing text node | text: string, node: Text | Node |
createGroup() | Creates a logical group for fragments | - | Group |
unwrapGroup(fragment) | Flattens a group to nodes | fragment: Group | Node[] |
createGroupHandle(group) | Creates stable handle for dynamic lists | group: Group | Handle |
getHandleNodes(handle) | Returns nodes associated with a handle | handle: Handle | Node[] |
write(handle, newContent) | Replaces content in a handle | handle: Handle, newContent: Node[] | void |
reconcile(handle, options) | Efficiently updates a dynamic list | handle: Handle, options: ReconcilerOptions | void |
append(container, children) | Attaches children to parent | parent: Node, children: Node | Node[] | Node |
setProperty(container, key, value) | Sets a property/attribute | node: N, key: string, value: unknown | N |
isNode(child) | Checks if value is a valid node | child: any | boolean |
isGroup(child) | Checks if value is a group | child: any | boolean |
isActive(node) | Checks if node is active | node: Node | boolean |
handleComponent(fn, props, snapshot?, fileData?) | Executes a component | fn: Function, props: any[], snapshot?: StateSnapshot, fileData?: JSX.JSXDevFileData | Node | 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. TheisReactiveflag 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.