dotnet/aspnetcore · error · Error

Cannot connect component ${this.localName} to the document a

Error message

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

What it means

Thrown from BlazorCustomElement.connectedCallback when the custom element is re-attached to the DOM after its disposal timer has already fired. BlazorCustomElement sets _isDisposed=true inside disconnectedCallback's setTimeout(1000) and then disposes the underlying Blazor root component, so re-inserting the same node back into the document is no longer legal because the .NET root component it was bound to no longer exists.

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 294cab2f9b)

Solutions

  1. If you must re-add the element, create a NEW element instance instead of reusing the disposed node (let the framework instantiate a fresh <my-component>).
  2. Ensure the element is re-inserted within the 1000ms disposal grace window so connectedCallback's clearTimeout cancels the disposal (the timeout is in disconnectedCallback at BlazorCustomElements.ts:77).
  3. Detach by moving the element into a hidden container rather than fully removing it from the document, so disconnectedCallback never fires.
  4. Call delete/cloneNode(false) and re-set attributes to obtain a fresh, undisposed custom element before re-insertion.

Example fix

// before: reusing a removed element after >1s
container.removeChild(myBlazorElement);
// ... >1000ms later ...
container.appendChild(myBlazorElement); // throws: already disposed

// after: create a fresh element
const fresh = document.createElement(myBlazorElement.localName);
fresh.setAttribute('some-param', 'value');
container.appendChild(fresh);
Defensive patterns

Strategy: try-catch

Validate before calling

// There is no public accessor for _isDisposed (private). Track externally.
const removedAt = Date.now();
container.removeChild(el);
// only re-add within the 1000ms disposal grace window
if (Date.now() - removedAt < 1000) {
  container.appendChild(el);
} else {
  const fresh = document.createElement(el.localName);
  // copy attributes
  for (const attr of el.attributes) fresh.setAttribute(attr.name, attr.value);
  container.appendChild(fresh);
}

Type guard

// BlazorCustomElement exposes no public 'disposed' flag.
// Duck-type by trying a no-op setParameter; disposed elements reject updates.
function isBlazorElementAlive(el: HTMLElement): boolean {
  return el.isConnected && !(el as unknown as { _isDisposed?: boolean })._isDisposed;
}

Try / catch

try {
  container.appendChild(existingElement);
} catch (e) {
  if (/after it has been disposed/.test((e as Error).message)) {
    const fresh = document.createElement(existingElement.localName);
    for (const a of existingElement.attributes) fresh.setAttribute(a.name, a.value);
    container.appendChild(fresh);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: A registered Blazor custom element is removed from the DOM and re-added more than 1000ms later (e.g. via conditional rendering that recreates the element, a virtual-list that recycles nodes, or a framework like Lit/React that moves DOM nodes). Re-insertion calls connectedCallback, which sees _isDisposed===true and throws.

Common situations: Toggling a container with *ngIf / React conditional that detaches the same element instance for over a second; dragging-and-dropping a custom element across containers with a delay; SPA frameworks that unmount then remount subtrees on navigation; using element.removeNode() followed by appendChild() in an async handler.

Related errors


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