Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

RenderScope and NodeHandle API

This document specifies the DOM abstraction layer that enables fine-grained reactive rendering.

Overview

The DOM abstraction consists of three key types:

TypePurpose
RenderScopeContext for building DOM trees with effect tracking
NodeHandleStable reference to a DOM node for surgical updates
DomDocumentTrait abstracting DOM mutation operations

RenderScope

RenderScope is the context passed to component functions. It provides methods for creating DOM nodes and Effects.

Creating Nodes

The recommended approach uses the #[component] macro, which injects __scope automatically:

#![allow(unused)]
fn main() {
#[component]
fn my_component() -> NodeHandle {
    rsx! {
        div {
            "Hello, world!"
        }
    }
}
}

For manual DOM construction, use __scope directly:

#![allow(unused)]
fn main() {
fn my_component(__scope: &mut RenderScope) -> NodeHandle {
    // Create an element node
    let div = __scope.create_element("div");

    // Create a text node
    let text = __scope.create_text("Hello, world!");

    // Create a comment node (useful for anchors)
    let comment = __scope.create_comment("placeholder");

    // Build the tree
    div.append_child(&text);

    div
}
}

Creating Effects

#![allow(unused)]
fn main() {
fn counter(__scope: &mut RenderScope) -> NodeHandle {
    let count = Signal::new(0);
    let span = __scope.create_element("span");

    // Create an Effect that updates the span when count changes
    // Signal and NodeHandle are Copy — no .clone() needed
    __scope.create_effect(move || {
        span.set_text(&count.get().to_string());
    });

    span
}
}

Child Scopes

Child scopes inherit the document but have their own Effect tracking. The child_scope method takes a parent NodeHandle reference and returns a mutable borrow:

#![allow(unused)]
fn main() {
fn parent(__scope: &mut RenderScope) -> NodeHandle {
    let container = __scope.create_element("div");

    // Create a child scope for a nested component
    let child_scope = __scope.child_scope(&container);
    let child_content = child_component(child_scope);
    container.append_child(&child_content);

    container
}
}

When a child scope is dropped, all its Effects are cleaned up.

Event Handling

Rinch uses a register_handler + data-rid pattern for event dispatch. There is no add_event_listener on NodeHandle. Instead, handlers are registered on the RenderScope and linked to elements via a data-rid attribute:

#![allow(unused)]
fn main() {
fn my_button(__scope: &mut RenderScope) -> NodeHandle {
    let button = __scope.create_element("button");
    button.set_text("Click me");

    // Register a handler and link it to the element
    let handler_id = __scope.register_handler(move || {
        println!("Button clicked!");
    });
    button.set_attribute("data-rid", &handler_id.to_string());

    button
}
}

The rsx! macro handles this automatically with onclick::

#![allow(unused)]
fn main() {
#[component]
fn my_button() -> NodeHandle {
    rsx! {
        button { onclick: move || println!("Clicked!"),
            "Click me"
        }
    }
}
}

The runtime uses event delegation (a single document-level listener) that dispatches to the correct handler by looking up the data-rid attribute on the clicked element.

RenderScope API Reference

MethodDescription
create_element(tag: &str) -> NodeHandleCreate an element node (div, span, etc.)
create_text(content: &str) -> NodeHandleCreate a text node
create_comment(content: &str) -> NodeHandleCreate a comment node
create_effect(f: impl FnMut() + 'static)Create a reactive Effect
child_scope(&mut self, parent: &NodeHandle) -> &mut RenderScopeCreate a child scope rooted at a parent node
register_handler(callback: impl Fn() + 'static) -> EventHandlerIdRegister an event handler, returns ID for data-rid
register_input_handler(callback: impl Fn(String) + 'static) -> EventHandlerIdRegister an input handler for text input events
parent() -> NodeHandleGet the parent node for this scope
doc_weak() -> Weak<RefCell<dyn DomDocument>>Get a weak reference to the underlying document
body_handle() -> NodeHandleGet the body element as a NodeHandle
dispose(self)Dispose this scope, cleaning up all Effects

NodeHandle

NodeHandle is a stable reference to a DOM node. It remains valid even as the document changes around it. Internally it holds a NodeId and a Weak<RefCell<dyn DomDocument>>.

Text Content

#![allow(unused)]
fn main() {
let text_node = __scope.create_text("initial");

// Update text content
text_node.set_text("updated");
}

Attributes

#![allow(unused)]
fn main() {
let button = __scope.create_element("button");

// Set attribute
button.set_attribute("disabled", "true");
button.set_attribute("aria-label", "Submit form");

// Get attribute
let label = button.get_attribute("aria-label"); // Some("Submit form")

// Remove attribute
button.remove_attribute("disabled");
}

Styles

#![allow(unused)]
fn main() {
let div = __scope.create_element("div");

// Set individual styles
div.set_style("color", "blue");
div.set_style("font-size", "16px");
div.set_style("display", "flex");

// Remove a style
div.set_style("color", "");  // Empty string removes
}

Classes

#![allow(unused)]
fn main() {
let element = __scope.create_element("div");

// Set the class attribute directly
element.set_class("active highlighted");

// Add/remove individual classes
element.add_class("active");
element.add_class("highlighted");
element.remove_class("active");

// Toggle based on condition
element.toggle_class("selected");

// Or conditionally:
if is_selected {
    element.add_class("selected");
} else {
    element.remove_class("selected");
}
}

Tree Manipulation

#![allow(unused)]
fn main() {
let parent = __scope.create_element("ul");
let item1 = __scope.create_element("li");
let item2 = __scope.create_element("li");
let item3 = __scope.create_element("li");

// Append children
parent.append_child(&item1);
parent.append_child(&item3);

// Insert before a reference node
parent.insert_before(&item2, &item3);  // item1, item2, item3

// Remove a node
item2.remove();  // item1, item3

// Replace a node
let new_item = __scope.create_element("li");
item1.replace_with(&new_item);
}

Focus

#![allow(unused)]
fn main() {
let input = __scope.create_element("input");
input.focus();  // Give focus to this element
}

NodeHandle API Reference

MethodDescription
set_text(content: &str)Set text content (for text nodes)
set_attribute(name: &str, value: &str)Set an attribute
get_attribute(name: &str) -> Option<String>Get an attribute value
remove_attribute(name: &str)Remove an attribute
set_style(property: &str, value: &str)Set a CSS style property
set_class(class: &str)Set the class attribute
add_class(name: &str)Add a CSS class
remove_class(name: &str)Remove a CSS class
toggle_class(name: &str)Toggle a CSS class
append_child(child: &NodeHandle)Append a child node
insert_before(node: &NodeHandle, reference: &NodeHandle)Insert before reference
remove()Remove this node from its parent
replace_with(new_node: &NodeHandle)Replace this node with another
focus()Give focus to this element
children() -> Vec<NodeHandle>Get child nodes as NodeHandles
is_valid() -> boolCheck if this handle still points to a valid node
node_id() -> NodeIdGet the internal node ID
clone() -> NodeHandleClone the handle (same underlying node)

DomDocument Trait

DomDocument is the trait that abstracts DOM operations. The primary desktop implementation is RinchDocument which uses Taffy + Parley + Vello. The web implementation is WebDocument which uses browser-native DOM via web_sys.

#![allow(unused)]
fn main() {
pub trait DomDocument {
    /// Create an element node
    fn create_element(&mut self, tag: &str) -> NodeId;

    /// Create a text node
    fn create_text(&mut self, content: &str) -> NodeId;

    /// Create a comment node
    fn create_comment(&mut self, content: &str) -> NodeId;

    /// Set text content of a node
    fn set_text_content(&mut self, node: NodeId, content: &str);

    /// Set an attribute
    fn set_attribute(&mut self, node: NodeId, name: &str, value: &str);

    /// Remove an attribute
    fn remove_attribute(&mut self, node: NodeId, name: &str);

    /// Append a child to a parent
    fn append_child(&mut self, parent: NodeId, child: NodeId);

    /// Insert a node before a reference node
    fn insert_before(&mut self, parent: NodeId, node: NodeId, reference: NodeId);

    /// Remove a node from its parent
    fn remove_child(&mut self, parent: NodeId, child: NodeId);

    /// Get children of a node
    fn get_children(&self, node: NodeId) -> Vec<NodeId>;

    /// Get the body element
    fn body(&self) -> NodeId;

    /// Mark a node as needing re-layout
    fn mark_dirty(&mut self, node: NodeId);
}
}

RinchDocument

RinchDocument is the desktop implementation of DomDocument that uses Taffy for layout, Parley for text, and Vello for rendering.

Key Features

  • Direct DOM manipulation - Efficient node creation and mutation
  • Automatic dirty marking - Calls mark_ancestors_dirty() after mutations
  • Event handler storage - Stores handlers as data-rid attributes for dispatch

Usage

#![allow(unused)]
fn main() {
use rinch_dom::RinchDocument;

// Create document
let doc = Rc::new(RefCell::new(RinchDocument::new()));

// Create RenderScope from shared document
let mut scope = RenderScope::new(Rc::downgrade(&doc) as _, parent_id);

// Build DOM
let root = my_app(&mut scope);
}

Thread Safety

RinchDocument is wrapped in Rc<RefCell<>> for interior mutability.

Effects capture clones of the shared document and can mutate the DOM when they run.

RenderScope itself holds a Weak<RefCell<dyn DomDocument>> to avoid preventing cleanup.

Integration with Reactive System

The RenderScope and NodeHandle APIs integrate with the reactive system:

  1. Initial render - Component function receives RenderScope (via __scope or #[component] macro), builds DOM tree
  2. Effect creation - __scope.create_effect() registers reactive computations
  3. NodeHandle capture - Effects capture NodeHandle clones for later updates
  4. Signal changes - Effects re-run and use NodeHandle methods to update DOM
  5. Cleanup - When scope is dropped, Effects are disposed

This architecture ensures that:

  • Components run once (no re-render overhead)
  • Updates are surgical (only affected nodes change)
  • Cleanup is automatic (scope disposal cleans up Effects)
  • Memory is efficient (NodeHandle is a lightweight ID + weak reference wrapper)