Documentation Index
Fetch the complete documentation index at: https://mintlify.com/DioxusLabs/dioxus/llms.txt
Use this file to discover all available pages before exploring further.
Dioxus’s renderer-agnostic architecture allows you to build custom renderers for any platform. The core mechanism is the WriteMutations trait, which translates VirtualDOM changes into platform-specific operations.
WriteMutations Trait
The WriteMutations trait is the bridge between Dioxus’s VirtualDOM and your rendering backend. It defines methods for creating, updating, and removing DOM elements.
Core Interface
pub trait WriteMutations {
fn append_children(&mut self, id: ElementId, m: usize);
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId);
fn create_placeholder(&mut self, id: ElementId);
fn create_text_node(&mut self, value: &str, id: ElementId);
fn load_template(&mut self, template: Template, index: usize, id: ElementId);
fn replace_node_with(&mut self, id: ElementId, m: usize);
fn insert_nodes_after(&mut self, id: ElementId, m: usize);
fn insert_nodes_before(&mut self, id: ElementId, m: usize);
fn set_attribute(&mut self, name: &'static str, ns: Option<&'static str>, value: &AttributeValue, id: ElementId);
fn set_node_text(&mut self, value: &str, id: ElementId);
fn create_event_listener(&mut self, name: &'static str, id: ElementId);
fn remove_event_listener(&mut self, name: &'static str, id: ElementId);
fn remove_node(&mut self, id: ElementId);
fn push_root(&mut self, id: ElementId);
}
Location: packages/core/src/mutations.rs:14-118
Building a Custom Renderer
Step 1: Define Your DOM Representation
First, create a data structure to represent your platform’s DOM:
use std::collections::HashMap;
use dioxus_core::ElementId;
struct CustomNode {
tag: String,
text: Option<String>,
attributes: HashMap<String, String>,
children: Vec<ElementId>,
}
struct CustomDom {
nodes: HashMap<usize, CustomNode>,
stack: Vec<ElementId>,
templates: HashMap<String, Template>,
}
impl CustomDom {
fn new() -> Self {
Self {
nodes: HashMap::new(),
stack: Vec::new(),
templates: HashMap::new(),
}
}
}
Step 2: Implement WriteMutations
Implement each mutation method to modify your DOM representation:
use dioxus_core::{WriteMutations, AttributeValue, Template};
impl WriteMutations for CustomDom {
fn append_children(&mut self, id: ElementId, m: usize) {
let children: Vec<_> = self.stack.drain(self.stack.len() - m..).collect();
if let Some(parent) = self.nodes.get_mut(&id.0) {
parent.children.extend(children);
}
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
// Navigate path to find node in template and assign ID
let template_root = self.stack.last().copied().unwrap();
let target = self.navigate_path(template_root, path);
// Clone the node at target and give it the new ID
if let Some(node) = self.nodes.get(&target.0).cloned() {
self.nodes.insert(id.0, node);
}
}
fn create_placeholder(&mut self, id: ElementId) {
let node = CustomNode {
tag: "placeholder".to_string(),
text: None,
attributes: HashMap::new(),
children: Vec::new(),
};
self.nodes.insert(id.0, node);
self.stack.push(id);
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
let node = CustomNode {
tag: "text".to_string(),
text: Some(value.to_string()),
attributes: HashMap::new(),
children: Vec::new(),
};
self.nodes.insert(id.0, node);
self.stack.push(id);
}
fn load_template(&mut self, template: Template, index: usize, id: ElementId) {
// Templates are pre-compiled DOM structures
// Clone the template and assign it the given ID
let template_node = self.clone_template(&template, index);
self.nodes.insert(id.0, template_node);
self.stack.push(id);
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
let new_nodes: Vec<_> = self.stack.drain(self.stack.len() - m..).collect();
// Find parent and replace old node with new nodes
if let Some((parent_id, idx)) = self.find_parent(id) {
if let Some(parent) = self.nodes.get_mut(&parent_id.0) {
parent.children.splice(idx..idx + 1, new_nodes);
}
}
self.nodes.remove(&id.0);
}
fn set_attribute(
&mut self,
name: &'static str,
_ns: Option<&'static str>,
value: &AttributeValue,
id: ElementId,
) {
if let Some(node) = self.nodes.get_mut(&id.0) {
match value {
AttributeValue::Text(s) => {
node.attributes.insert(name.to_string(), s.clone());
}
AttributeValue::Bool(b) => {
node.attributes.insert(name.to_string(), b.to_string());
}
AttributeValue::Int(i) => {
node.attributes.insert(name.to_string(), i.to_string());
}
AttributeValue::Float(f) => {
node.attributes.insert(name.to_string(), f.to_string());
}
AttributeValue::None => {
node.attributes.remove(name);
}
_ => {}
}
}
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
if let Some(node) = self.nodes.get_mut(&id.0) {
node.text = Some(value.to_string());
}
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
// Register event listener for this node
// Implementation depends on your event system
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
// Unregister event listener
}
fn remove_node(&mut self, id: ElementId) {
self.nodes.remove(&id.0);
}
fn push_root(&mut self, id: ElementId) {
self.stack.push(id);
}
}
Step 3: Create the Renderer Loop
Integrate with VirtualDOM to handle updates:
use dioxus::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
let mut custom_dom = CustomDom::new();
// Initial render
dom.rebuild(&mut custom_dom);
render_to_output(&custom_dom);
// Event loop
loop {
// Wait for work (events, signals, etc.)
futures::executor::block_on(dom.wait_for_work());
// Render changes
dom.render_immediate(&mut custom_dom);
render_to_output(&custom_dom);
}
}
fn app() -> Element {
rsx! {
div { "Hello from custom renderer!" }
}
}
fn render_to_output(dom: &CustomDom) {
// Convert your custom DOM to actual output
// (terminal, GPU commands, network protocol, etc.)
}
Understanding Mutations
Dioxus generates mutations in a specific order that forms a stack-based execution model:
Stack Operations
The mutation stack is used to pass nodes between operations:
// Creating nodes pushes them onto the stack
CreateTextNode { value: "Hello", id: ElementId(1) } // Stack: [1]
CreateTextNode { value: "World", id: ElementId(2) } // Stack: [1, 2]
// AppendChildren pops m nodes from stack and adds to parent
AppendChildren { id: ElementId(0), m: 2 } // Stack: [] (consumed 1 and 2)
Template System
Templates are pre-compiled DOM structures that can be efficiently cloned:
rsx! {
div { class: "container",
p { "Static text" }
}
}
Compiles to a template that can be instantiated with LoadTemplate instead of creating each element individually.
Reference: packages/core/src/mutations.rs:120-282
Real-World Examples
Terminal Renderer (TUI)
For a TUI renderer, you’d map mutations to terminal operations:
use crossterm::{cursor, style, QueueableCommand};
use std::io::stdout;
impl WriteMutations for TuiRenderer {
fn create_text_node(&mut self, value: &str, id: ElementId) {
let node = TuiNode::Text {
content: value.to_string(),
position: self.calculate_position(id),
};
self.nodes.insert(id, node);
self.needs_redraw = true;
}
fn set_attribute(&mut self, name: &'static str, _ns: Option<&'static str>, value: &AttributeValue, id: ElementId) {
match name {
"color" => self.set_color(id, value),
"style" => self.set_style(id, value),
_ => {}
}
}
}
GPU Renderer
For GPU rendering, mutations would generate draw commands:
impl WriteMutations for GpuRenderer {
fn create_element(&mut self, tag: &str, id: ElementId) {
match tag {
"rect" => self.add_quad(id),
"circle" => self.add_circle(id),
"text" => self.add_text_mesh(id),
_ => {}
}
}
fn set_attribute(&mut self, name: &'static str, _ns: Option<&'static str>, value: &AttributeValue, id: ElementId) {
match name {
"x" | "y" | "width" | "height" => self.update_transform(id, name, value),
"color" => self.update_material(id, value),
_ => {}
}
}
}
Event Handling
Custom renderers need to send events back to the VirtualDOM:
use dioxus_core::Event;
use std::rc::Rc;
// When an event occurs in your renderer:
fn handle_click(&self, element_id: ElementId, x: i32, y: i32) {
let data = Rc::new((x, y)) as Rc<dyn Any>;
let event = Event::new(data, true /* bubbles */);
self.runtime.handle_event("onclick", event, element_id);
}
Reference: packages/core/src/virtual_dom.rs:97-107
Best Practices
- Batch Rendering: Collect all mutations before updating the actual display
- Template Caching: Cache compiled templates for fast instantiation
- Incremental Updates: Only update what changed, not the entire tree
- Event Deduplication: Coalesce multiple events before processing
- Memory Management: Clean up removed nodes to prevent leaks
Testing Your Renderer
Use NoOpMutations for testing without a real renderer:
use dioxus_core::NoOpMutations;
#[test]
fn test_component() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut NoOpMutations);
// Mutations are ignored, useful for testing logic
}
Reference: packages/core/src/mutations.rs:383-423
Additional Resources
- Web Renderer:
packages/web/src/dom.rs
- Desktop Renderer:
packages/desktop/src/ (uses webview)
- Native Renderer:
packages/native/src/ (GPU-based)
- Liveview Renderer:
packages/liveview/src/ (WebSocket streaming)