Skip to main content

Overview

The VirtualDom is the core engine that manages component state, tracks updates, and generates mutations for renderers. It implements a concurrent, async-aware virtual DOM with support for suspense, error boundaries, and efficient diffing.

VirtualDom Structure

pub struct VirtualDom {
    scopes: Slab<ScopeState>,
    dirty_scopes: BTreeSet<ScopeOrder>,
    runtime: Rc<Runtime>,
    resolved_scopes: Vec<ScopeId>,
    rx: UnboundedReceiver<SchedulerMsg>,
}

Creating a VirtualDom

Basic Creation

use dioxus::prelude::*;

fn app() -> Element {
    rsx! { div { "Hello, world!" } }
}

let mut dom = VirtualDom::new(app);

With Props

use dioxus::prelude::*;

#[derive(PartialEq, Props, Clone)]
struct AppProps {
    title: String,
}

fn app(props: AppProps) -> Element {
    rsx! { h1 { "{props.title}" } }
}

let mut dom = VirtualDom::new_with_props(
    app,
    AppProps { title: "My App".to_string() },
);

Prebuilt VirtualDom

// Create and immediately build the initial tree
let mut dom = VirtualDom::prebuilt(app);

With Root Context

#[derive(Clone)]
struct AppConfig {
    api_url: String,
}

let dom = VirtualDom::new(app)
    .with_root_context(AppConfig {
        api_url: "https://api.example.com".to_string(),
    });

Rendering

Initial Render

use dioxus::prelude::*;

let mut dom = VirtualDom::new(app);
let mut mutations = Mutations::default();

// Generate the initial UI
dom.rebuild(&mut mutations);

// Apply mutations to your renderer
for edit in mutations.edits {
    // Handle edit
}

Render to Vec

// For testing or simple cases
let mutations = dom.rebuild_to_vec();

In-place Rebuild

// Rebuild without collecting mutations
dom.rebuild_in_place();

Update Cycle

Immediate Rendering

use dioxus::prelude::*;

let mut dom = VirtualDom::new(app);
let mut mutations = Mutations::default();

dom.rebuild(&mut mutations);

loop {
    // Wait for work
    dom.wait_for_work().await;
    
    // Render all dirty components
    mutations.edits.clear();
    dom.render_immediate(&mut mutations);
    
    // Apply mutations
    for edit in &mutations.edits {
        // Handle edit
    }
}

Render to Vec

// For testing
let mutations = dom.render_immediate_to_vec();

Event Loop

A complete event loop integrating VirtualDom with a renderer:
use dioxus::prelude::*;

#[component]
fn app() -> Element {
    let mut count = use_signal(|| 0);
    
    rsx! {
        button {
            onclick: move |_| count += 1,
            "Count: {count}"
        }
    }
}

async fn run() {
    let mut dom = VirtualDom::new(app);
    let mut mutations = Mutations::default();
    
    // Initial render
    dom.rebuild(&mut mutations);
    apply_mutations(&mutations);
    
    loop {
        tokio::select! {
            // Wait for component updates
            _ = dom.wait_for_work() => {
                mutations.edits.clear();
                dom.render_immediate(&mut mutations);
                apply_mutations(&mutations);
            }
            
            // Handle user events
            event = wait_for_event() => {
                let evt = Event::new(event.data, true);
                dom.runtime().handle_event(
                    &event.name,
                    evt,
                    event.element_id,
                );
            }
        }
    }
}

Suspense

Wait for Suspense

use dioxus::prelude::*;

let mut dom = VirtualDom::new(app);

// Build initial tree
dom.rebuild_in_place();

// Wait for all suspense boundaries to resolve
dom.wait_for_suspense().await;

// Now the tree is fully rendered
let mutations = dom.rebuild_to_vec();

Check Suspended Tasks

if dom.suspended_tasks_remaining() {
    println!("Still waiting for suspended tasks");
}

Render During Suspense

// Render scopes that are ready, even during suspense
let resolved = dom.render_suspense_immediate().await;

for scope_id in resolved {
    println!("Resolved scope: {:?}", scope_id);
}

Scope Management

Access Scopes

// Get a specific scope
if let Some(scope) = dom.get_scope(scope_id) {
    let node = scope.root_node();
    // Work with scope
}

// Get the root scope
let root = dom.base_scope();

Mark Dirty

// Manually mark a scope for re-render
dom.mark_dirty(scope_id);

// Mark all scopes dirty
dom.mark_all_dirty();

Run in Runtime Context

dom.in_runtime(|| {
    // Code runs with runtime active
    let signal = use_signal(|| 0);
});

Run in Scope

dom.in_scope(scope_id, || {
    // Code runs in this scope's context
});

Context Injection

Provide Root Context

#[derive(Clone)]
struct Config {
    debug: bool,
}

// During creation
let dom = VirtualDom::new(app)
    .with_root_context(Config { debug: true });

// After creation
dom.provide_root_context(Config { debug: false });

// Type-erased context
dom.insert_any_root_context(Box::new(Config { debug: true }));

Runtime Access

let runtime = dom.runtime();

// Handle events
let event = Event::new(event_data, true);
runtime.handle_event("onclick", event, element_id);

// Spawn tasks
runtime.spawn(async {
    // Async work
});

Diffing Algorithm

The VirtualDom uses an efficient diffing algorithm that:
  1. Templates are compared by pointer - Static templates are only created once
  2. Dynamic nodes are diffed recursively - Only changed parts update
  3. Keys enable list optimization - Reordering lists is efficient

Example: List Diffing

#[component]
fn TodoList(todos: Vec<Todo>) -> Element {
    rsx! {
        ul {
            // Keys help diff algorithm track items
            for todo in todos {
                li { key: "{todo.id}",
                    "{todo.text}"
                }
            }
        }
    }
}

Mutations

The Mutations struct contains the list of edits to apply:
pub struct Mutations {
    pub edits: Vec<Mutation>,
    pub templates: Vec<Template>,
}

pub enum Mutation {
    CreateElement { tag: &'static str, id: ElementId },
    CreateTextNode { text: String, id: ElementId },
    SetAttribute { name: &'static str, value: AttributeValue, id: ElementId },
    RemoveAttribute { name: &'static str, id: ElementId },
    AppendChildren { id: ElementId, m: usize },
    ReplaceWith { id: ElementId, m: usize },
    Remove { id: ElementId },
    // ... more variants
}

Processing Mutations

use dioxus::prelude::*;

struct MyRenderer;

impl WriteMutations for MyRenderer {
    fn append_children(&mut self, id: ElementId, m: usize) {
        // Append m children from stack to element
    }
    
    fn create_element(&mut self, tag: &str, id: ElementId) {
        // Create element and push to stack
    }
    
    fn create_text_node(&mut self, text: &str, id: ElementId) {
        // Create text node and push to stack
    }
    
    // ... implement other methods
}

Advanced Features

Process Events

// Process all queued events synchronously
dom.process_events();

Wait for Work

// Async wait for any work (events, tasks, signals)
dom.wait_for_work().await;

Check for Dirty Scopes

if dom.has_dirty_scopes() {
    // Render needed
}

Testing

use dioxus::prelude::*;

#[test]
fn test_component() {
    fn app() -> Element {
        rsx! { div { "Hello" } }
    }
    
    let mut dom = VirtualDom::new(app);
    let mutations = dom.rebuild_to_vec();
    
    // Assert mutations are correct
    assert_eq!(mutations.edits.len(), 3);
}

Async Testing

#[tokio::test]
async fn test_suspense() {
    fn app() -> Element {
        let resource = use_resource(|| async {
            // Fetch data
        });
        
        rsx! {
            match resource() {
                Some(data) => rsx! { div { "{data}" } },
                None => rsx! { div { "Loading..." } },
            }
        }
    }
    
    let mut dom = VirtualDom::new(app);
    dom.rebuild_in_place();
    dom.wait_for_suspense().await;
    
    let mutations = dom.rebuild_to_vec();
    // Assert rendered correctly
}

Performance

Memoization

// Components with PartialEq props are memoized
#[derive(Props, PartialEq, Clone)]
struct ExpensiveProps {
    data: Vec<i32>,
}

#[component]
fn Expensive(props: ExpensiveProps) -> Element {
    // Only re-renders when props.data changes
    rsx! { div { "Rendered" } }
}

Keys for Lists

// Always use keys for dynamic lists
rsx! {
    for item in items {
        div { key: "{item.id}", "{item.name}" }
    }
}

See Also

Build docs developers (and LLMs) love