{"record":{"id":"87153a6e59becc42","repo":"dotnet/aspnetcore","slug":"cannot-connect-component-this-localname-to-the","errorCode":null,"errorMessage":"Cannot connect component ${this.localName} to the document after it has been disposed.","messagePattern":"Cannot connect component (.+?) to the document after it has been disposed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/Components/CustomElements/src/js/BlazorCustomElements.ts","lineNumber":70,"sourceCode":"      Object.defineProperty(this, camelCase(dotNetName), {\n        get: () => this._parameterValues[dotNetName],\n        set: newValue => {\n          if (this.hasAttribute(attributeName)) {\n            // It's nice to keep the DOM in sync with the properties. This set a string representation\n            // of the value, but this will get overwritten with the original typed value before we send it to .NET\n            this.setAttribute(attributeName, newValue);\n          }\n\n          this._parameterValues[dotNetName] = newValue;\n          this._supplyUpdatedParameters();\n        }\n      });\n    }\n  }\n\n  connectedCallback() {\n    if (this._isDisposed) {\n      throw new Error(`Cannot connect component ${this.localName} to the document after it has been disposed.`);\n    }\n\n    clearTimeout(this._disposalTimeoutHandle);\n  }\n\n  disconnectedCallback() {\n    this._disposalTimeoutHandle = setTimeout(async () => {\n      this._isDisposed = true;\n      const rootComponent = await this._addRootComponentPromise;\n      rootComponent.dispose();\n    }, 1000);\n  }\n\n  attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    const parameterInfo = this._attributeMappings[name];\n    if (parameterInfo) {\n      this._parameterValues[parameterInfo.name] = BlazorCustomElement.parseAttributeValue(newValue, parameterInfo.type, parameterInfo.name);\n      this._supplyUpdatedParameters();","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/dotnet/aspnetcore/blob/294cab2f9b2e03af6b953820c7ab497c3c8b7ad9/src/Components/CustomElements/src/js/BlazorCustomElements.ts#L52-L88","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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>).","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).","Detach by moving the element into a hidden container rather than fully removing it from the document, so disconnectedCallback never fires.","Call delete/cloneNode(false) and re-set attributes to obtain a fresh, undisposed custom element before re-insertion."],"exampleFix":"// before: reusing a removed element after >1s\ncontainer.removeChild(myBlazorElement);\n// ... >1000ms later ...\ncontainer.appendChild(myBlazorElement); // throws: already disposed\n\n// after: create a fresh element\nconst fresh = document.createElement(myBlazorElement.localName);\nfresh.setAttribute('some-param', 'value');\ncontainer.appendChild(fresh);","handlingStrategy":"try-catch","validationCode":"// There is no public accessor for _isDisposed (private). Track externally.\nconst removedAt = Date.now();\ncontainer.removeChild(el);\n// only re-add within the 1000ms disposal grace window\nif (Date.now() - removedAt < 1000) {\n  container.appendChild(el);\n} else {\n  const fresh = document.createElement(el.localName);\n  // copy attributes\n  for (const attr of el.attributes) fresh.setAttribute(attr.name, attr.value);\n  container.appendChild(fresh);\n}","typeGuard":"// BlazorCustomElement exposes no public 'disposed' flag.\n// Duck-type by trying a no-op setParameter; disposed elements reject updates.\nfunction isBlazorElementAlive(el: HTMLElement): boolean {\n  return el.isConnected && !(el as unknown as { _isDisposed?: boolean })._isDisposed;\n}","tryCatchPattern":"try {\n  container.appendChild(existingElement);\n} catch (e) {\n  if (/after it has been disposed/.test((e as Error).message)) {\n    const fresh = document.createElement(existingElement.localName);\n    for (const a of existingElement.attributes) fresh.setAttribute(a.name, a.value);\n    container.appendChild(fresh);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Never re-insert a Blazor custom element after it has been detached for more than 1 second.","Prefer creating a fresh element over recycling a removed one.","Move elements to a hidden node instead of detaching them, to avoid triggering disposal.","Keep a 1-second mental budget: disconnectedCallback schedules disposal after 1000ms."],"tags":["blazor-custom-elements","lifecycle","dom","custom-element"],"analyzedSha":"294cab2f9b2e03af6b953820c7ab497c3c8b7ad9","analyzedAt":"2026-08-06T20:08:02.189Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}