Skip to content

bug(aria/tree): NG0950 when tree items or groups are destroyed before first change detection #34058

Open

Description

@nagahara-naoki

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

TreeItem and TreeItemGroup throw NG0950 when a component containing them is destroyed synchronously after TestBed.createComponent(), before its first change detection.

The required inputs are bound in the template. However, ngOnInit has not run and the input bindings have not been applied, while ngOnDestroy still reads them when attempting to unregister.

This affects early teardown, for example a unit test that creates a component and destroys it without running change detection. I reproduced this in the repository Chromium test runner. I have not verified how frequently it occurs in production applications; ordinary initialized trees are not affected by this reproduction.

Reproduction

The minimal failing operation, using either fixture defined below, is:

const fixture = TestBed.createComponent(StaticFlatTree);
fixture.destroy(); // NG0950: required input "parent" has not been set yet.

With StaticNestedTree, the same operation reports the required ownedBy input.

This is a synchronous creation/teardown lifecycle reproduction rather than a visible interaction, so I am including complete TestBed fixtures and the exact repository test command instead of an interactive StackBlitz.

Repository setup

  1. Check out angular/components at e59fce84b.
  2. Install the Node.js version in .nvmrc and pnpm from package.json#packageManager, then run pnpm install --frozen-lockfile.
  3. In src/aria/tree/tree.spec.ts, add the following fixtures and tests as a separate top-level describe, outside the existing describe('Tree') (its afterEach expects its own initialized fixture). The imports listed below are already present; reuse them instead of adding duplicate imports.
  4. Run pnpm test src/aria/tree --no-watch.
// Already imported in tree.spec.ts:
// import {Component, ChangeDetectionStrategy} from "@angular/core";
// import {TestBed} from "@angular/core/testing";
// import {Tree} from "./tree";
// import {TreeItem} from "./tree-item";
// import {TreeItemGroup} from "./tree-item-group";

@Component({
  template: `
    <ul ngTree #tree="ngTree">
      <li ngTreeItem [parent]="tree" value="item">Item</li>
    </ul>
  `,
  imports: [Tree, TreeItem],
  changeDetection: ChangeDetectionStrategy.Eager,
})
class StaticFlatTree {}

@Component({
  template: `
    <ul ngTree #tree="ngTree">
      <li ngTreeItem [parent]="tree" value="parent" #parentItem="ngTreeItem">
        Parent
        <ul role="group">
          <ng-template ngTreeItemGroup [ownedBy]="parentItem" #group="ngTreeItemGroup">
            <li ngTreeItem [parent]="group" value="child">Child</li>
          </ng-template>
        </ul>
      </li>
    </ul>
  `,
  imports: [Tree, TreeItem, TreeItemGroup],
  changeDetection: ChangeDetectionStrategy.Eager,
})
class StaticNestedTree {}

describe("Tree destruction", () => {
  it("destroys a flat tree before first change detection", () => {
    const fixture = TestBed.createComponent(StaticFlatTree);
    expect(() => fixture.destroy()).not.toThrow();
  });

  it("destroys a nested tree before first change detection", () => {
    const fixture = TestBed.createComponent(StaticNestedTree);
    expect(() => fixture.destroy()).not.toThrow();
  });

  it("destroys an initialized flat tree (control)", async () => {
    const fixture = TestBed.createComponent(StaticFlatTree);
    await fixture.whenStable();
    expect(() => fixture.destroy()).not.toThrow();
  });

  it("destroys an initialized nested tree (control)", async () => {
    const fixture = TestBed.createComponent(StaticNestedTree);
    await fixture.whenStable();
    expect(() => fixture.destroy()).not.toThrow();
  });
});

Do not call fixture.detectChanges() or await fixture.whenStable() before destroy() in the first two tests: that initializes the bindings and bypasses the reproduction. Use static template elements; putting the items inside @if/@for changes when they are created.

The ngTreeItemGroup directive itself is created with the static template, even though its child item inside ng-template is deferred.

Expected Behavior

Destroying before initialization should complete without throwing. Nothing was registered in ngOnInit, so there should be nothing to unregister. Destruction after initialization should continue to unregister normally.

Actual Behavior

The flat-tree test throws:

NG0950: Input "parent" is required but no value is available yet.

The nested-tree test throws:

NG0950: Input "ownedBy" is required but no value is available yet.

With the four tests in the reproduction, the Chromium suite reports 144 passed, 2 failed. Only the two pre-initialization tests fail; both initialized control tests pass.

Relevant cleanup code

TreeItem.ngOnDestroy() reads parent() and tree(), and TreeItemGroup.ngOnDestroy() reads ownedBy(). Registration happens in ngOnInit.

Related issue and PR

This appears to be the same lifecycle issue as #33972, fixed for combobox in #33974. A corresponding tree fix can retain the references used during registration and only unregister them if initialization registered them. The implementation and regression tests are in #34059.

This report covers @angular/aria/tree only.

Environment

  • Components source: angular/components at e59fce84b (2026-11-04)
  • Angular framework packages used by the repository: 22.3.0-next.3
  • Browser: Chromium provided by the repository Bazel browser test setup
  • Operating system: macOS, Darwin 25.5.0, arm64
  • Node.js: 22.23.4 (.nvmrc)
  • pnpm: 12.16.1 (packageManager)
  • Command: pnpm test src/aria/tree --no-watch
  • Regression status: unknown; older releases have not been tested.

Activity

  1. added a commit that references this issue on Nov 4, 2026
    1051f2a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions