DioxusLabs/dioxus · error

The tree has not been built yet. Make sure to call rebuild o

Error message

The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.

What it means

ScopeState::root_node() returns the element a scope last rendered, but a freshly constructed VirtualDom has not run a render pass yet, so last_rendered_node is None and the expect panics. The method's docs explicitly state it panics unless the tree has been built. This is a lifecycle error: build the tree before traversing it.

Source

Thrown at packages/core/src/scopes.rs:91

pub struct ScopeState {
    pub(crate) runtime: Rc<Runtime>,
    pub(crate) context_id: ScopeId,
    /// The last node that has been rendered for this component. This node may not ben mounted
    /// During suspense, this component can be rendered in the background multiple times
    pub(crate) last_rendered_node: Option<LastRenderedNode>,
    pub(crate) props: BoxedAnyProps,
    pub(crate) reactive_context: ReactiveContext,
}

impl ScopeState {
    /// Get a handle to the currently active head node arena for this Scope
    ///
    /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
    ///
    /// Panics if the tree has not been built yet.
    pub fn root_node(&self) -> &VNode {
        self.try_root_node()
            .expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.")
    }

    /// Try to get a handle to the currently active head node arena for this Scope
    ///
    /// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
    ///
    /// Returns [`None`] if the tree has not been built yet.
    pub fn try_root_node(&self) -> Option<&VNode> {
        match &self.last_rendered_node {
            Some(LastRenderedNode::Real(vnode)) => Some(vnode),
            Some(LastRenderedNode::Placeholder(vnode, _)) => Some(vnode),
            None => None,
        }
    }

    /// Returns the scope id of this [`ScopeState`].
    pub fn id(&self) -> ScopeId {
        self.context_id

View on GitHub (pinned to 393d190a80)

Solutions

  1. Call vdom.rebuild_in_place() (or run the rebuild flow in your renderer) before touching scopes
  2. Use scope.try_root_node() and skip scopes that return None instead of root_node()
  3. For suspended scopes, wait until they resolve before traversal

Example fix

// before
let vdom = VirtualDom::new(App);
let node = vdom.scopes()[0].root_node(); // panics
// after
let mut vdom = VirtualDom::new(App);
vdom.rebuild_in_place();
let node = vdom.scopes()[0].root_node();
Defensive patterns

Strategy: validation

Validate before calling

// Guard traversal with the non-panicking variant:
for scope in vdom.scopes() {
    if let Some(root) = scope.try_root_node() {
        visit(root);
    } // skip scopes not built yet (e.g. suspended)
}

Type guard

fn scope_is_built(scope: &dioxus_core::ScopeState) -> bool {
    scope.try_root_node().is_some()
}

Prevention

When it happens

Trigger: Calling scope.root_node() (custom renderers, SSR walkers, tests, html rendering APIs) on a VirtualDom created with VirtualDom::new but never passed through rebuild()/rebuild_in_place(); accessing root_node before the first render commit.

Common situations: Writing a custom renderer or SSR integration and iterating vdom.scopes() before rebuild; unit tests that construct a VirtualDom and immediately inspect its nodes.

Related errors


AI-assisted analysis of DioxusLabs/dioxus@393d190a80 (2026-08-16). Data as JSON: /api/errors/d300662f31b168bb. Report an issue: GitHub.