dotnet/aspnetcore · error · Error

Cannot connect component

Error message

Cannot connect component ${this.localName} to the document after it has been disposed.

What it means

Thrown by BlazorCustomElement.connectedCallback when a custom element is re-inserted into the DOM after its disposal timer has already fired. disconnectedCallback schedules disposal after a 1000ms delay; once that callback runs, _isDisposed becomes true and the underlying .NET root component is disposed. Reconnecting the same element node after that point cannot resurrect a disposed .NET component, so the framework refuses rather than silently breaking.

Solutions

  1. Render a fresh element instance instead of reusing the disposed node: let Blazor diff create a new custom element rather than moving the old DOM node.
  2. Avoid removing the element from the DOM if it will be needed again; prefer hiding via CSS (display:none) over detach.
  3. If you must detach/reattach within the same lifetime, ensure the reconnection happens within the 1000ms disposal window — but treat this as fragile and prefer recreating the element.
  4. Audit framework-level node recycling (e.g. virtualization components) to confirm they create new elements rather than reusing detached ones.

Example fix

// before: moving a disposed node back into the DOM
container.appendChild(existingElement); // throws if _isDisposed

// after: create a new element so Blazor registers a fresh root component
const fresh = document.createElement(existingElement.localName);
// copy over the attributes you need
for (const attr of existingElement.attributes) fresh.setAttribute(attr.name, attr.value);
container.appendChild(fresh);
Defensive patterns

Strategy: validation

Validate before calling

// Before reattaching a previously-removed element, check whether disposal already fired.
function safeReattach(parent, node) {
  if (node._isDisposed) {
    // Recreate a fresh element instead of reusing the disposed one.
    const fresh = document.createElement(node.localName);
    for (const a of node.attributes) fresh.setAttribute(a.name, a.value);
    parent.appendChild(fresh);
    return fresh;
  }
  parent.appendChild(node);
  return node;
}

Type guard

// Framework element is a BlazorCustomElement; the guard checks the disposal flag.
function isDisposedBlazorElement(node) {
  return node instanceof HTMLElement && node._isDisposed === true;
}

Try / catch

try {
  container.appendChild(existingElement);
} catch (e) {
  if (/has been disposed/.test(e.message)) {
    // Fall back to a freshly created element.
    const fresh = document.createElement(existingElement.localName);
    container.appendChild(fresh);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: An element is removed from the DOM (e.g. via a parent removal, conditional render, or list diff) and then re-added more than 1000ms later, OR the same element node is moved between documents after disposal. Also triggered by re-rendering logic that detaches and reattaches the same node across a long pause.

Common situations: Drag-and-drop that detaches/re-attaches nodes; virtualized lists that recycle nodes after idle periods; SPA route transitions that physically remove and later restore a host element; tabs/accordions that toggle display:none vs actual DOM removal with delayed reconnect.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/87153a6e59becc42. Report an issue: GitHub.

Appendix: source

Thrown at src/Components/CustomElements/src/js/BlazorCustomElements.ts:70

      Object.defineProperty(this, camelCase(dotNetName), {
        get: () => this._parameterValues[dotNetName],
        set: newValue => {
          if (this.hasAttribute(attributeName)) {
            // It's nice to keep the DOM in sync with the properties. This set a string representation
            // of the value, but this will get overwritten with the original typed value before we send it to .NET
            this.setAttribute(attributeName, newValue);
          }

          this._parameterValues[dotNetName] = newValue;
          this._supplyUpdatedParameters();
        }
      });
    }
  }

  connectedCallback() {
    if (this._isDisposed) {
      throw new Error(`Cannot connect component ${this.localName} to the document after it has been disposed.`);
    }

    clearTimeout(this._disposalTimeoutHandle);
  }

  disconnectedCallback() {
    this._disposalTimeoutHandle = setTimeout(async () => {
      this._isDisposed = true;
      const rootComponent = await this._addRootComponentPromise;
      rootComponent.dispose();
    }, 1000);
  }

  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    const parameterInfo = this._attributeMappings[name];
    if (parameterInfo) {
      this._parameterValues[parameterInfo.name] = BlazorCustomElement.parseAttributeValue(newValue, parameterInfo.type, parameterInfo.name);
      this._supplyUpdatedParameters();

View on GitHub (pinned to 3600ca084e)