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
- 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.
- Avoid removing the element from the DOM if it will be needed again; prefer hiding via CSS (display:none) over detach.
- 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.
- 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
- Prefer creating new elements over moving old nodes between parents.
- Hide elements with CSS instead of detaching when they may be needed again.
- If you must detach/reattach, do so well within the 1000ms disposal window.
- Audit virtualization and list-diffing code to confirm it does not recycle disposed nodes.
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
- Blazor has already started.
- Blazor has already started.
- Blazor has already started.
- Blazor has already started.
- Blazor Server has already started.
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)