Skip to content

Renderer Integration

wirelessEye edited this page Jun 29, 2026 · 1 revision

Renderer Integration

Nestix core does not render to any platform. A renderer is a set of components that translate Nestix lifecycle and placement events into host operations.

This page describes the renderer contract using DOM-like examples. The same pattern applies to other host targets.

Root Component

A root component should provide the top-level host handle and render its children.

#[props(debug)]
#[derive(Debug)]
pub struct RootProps {
    #[props(default)]
    children: Layout,
}

#[component]
pub fn Root(props: &RootProps, element: &Element) -> Element {
    let root_node = find_root_node();
    element.provide_handle(root_node);

    layout! {
        Fragment {
            $(props.children.clone())
        }
    }
}

mount_root(&layout! { Root { App } }) mounts the Nestix tree. Host children can then find the root handle through placement.

Host Node Component

A host component should:

  • create the host object during mount;
  • register placement behavior;
  • synchronize reactive props with effects;
  • register unmount cleanup;
  • provide the host handle;
  • render children, usually through Fragment.
#[props(debug)]
#[derive(Debug)]
pub struct DivProps {
    class: Option<String>,
    #[props(default)]
    children: Layout,
}

#[component]
pub fn Div(props: &DivProps, element: &Element) -> Element {
    let node = create_div();

    element.on_place(closure!([node] |placement| {
        place_node(&node, placement);
    }));

    effect!([node, props.class] || {
        set_class(&node, class.get());
    });

    element.on_unmount(closure!([node] || {
        remove_node(&node);
    }));

    element.provide_handle(node.clone());

    layout! {
        Fragment {
            $(props.children.clone())
        }
    }
}

Text Component

Text nodes are usually leaf components. They still provide a handle so later siblings can be placed after them.

#[props(debug)]
#[derive(Debug)]
pub struct TextProps {
    #[props(start)]
    text: String,
}

#[component]
pub fn Text(props: &TextProps, element: &Element) {
    let node = create_text_node(&props.text.get());

    element.on_place(closure!([node] |placement| {
        place_node(&node, placement);
    }));

    effect!([node, props.text] || {
        set_text(&node, &text.get());
    });

    element.on_unmount(closure!([node] || {
        remove_node(&node);
    }));

    element.provide_handle(node);
}

This component returns (), because it creates a host leaf and has no Nestix children.

Placement Algorithm

Renderer placement usually follows this order:

  1. If placement.pred exists, insert the host node after that predecessor.
  2. Else if placement.parent exists, append the host node to that parent.
  3. Else this node is at an unattached root and the renderer may do nothing.

pred and parent are Shared<dyn Any>, so the renderer must downcast to the host types it supports.

Event Callbacks

Use callback! for user callbacks and renderer event adapters.

#[props(debug)]
#[derive(Debug)]
pub struct ButtonProps {
    on_click: Option<Shared<dyn Fn()>>,
}

effect!([node, props.on_click] || {
    if let Some(on_click) = on_click.get() {
        attach_click_listener(&node, callback!([on_click] || on_click()));
    }
});

When host APIs require removing previous listeners, store listener handles in renderer-owned state and replace them inside an effect.

Reactive Prop Synchronization

Renderer components should put host updates in effects:

effect!([input, props.value] || {
    input.set_value(&value.get());
});

Reading props.value.get() inside the effect subscribes the effect to future changes when the prop was signal-backed.

Cleanup

Always remove host objects and long-lived event listeners in on_unmount.

element.on_unmount(closure!([node, listener_id] || {
    remove_listener(listener_id);
    remove_node(&node);
}));

This keeps renderer resources aligned with the Nestix tree.

Clone this wiki locally