angular/components · error · Error

ListKeyManager constructed with a signal must receive an inj

Error message

ListKeyManager constructed with a signal must receive an injector

What it means

ListKeyManager can take a Signal as its items source, but signal-based item changes are tracked with an effect() that requires an injection context. When constructed with a signal outside an injection context, an Injector must be passed explicitly; otherwise dev-mode throws this error.

Source

Thrown at src/cdk/a11y/key-manager/list-key-manager.ts:76

   */
  private _skipPredicateFn = (item: T) => item.disabled;

  constructor(items: QueryList<T> | T[] | readonly T[]);
  constructor(items: Signal<T[]> | Signal<readonly T[]>, injector: Injector);
  constructor(
    private _items: QueryList<T> | T[] | readonly T[] | Signal<T[]> | Signal<readonly T[]>,
    injector?: Injector,
  ) {
    // We allow for the items to be an array because, in some cases, the consumer may
    // not have access to a QueryList of the items they want to manage (e.g. when the
    // items aren't being collected via `ViewChildren` or `ContentChildren`).
    if (_items instanceof QueryList) {
      this._itemChangesSubscription = _items.changes.subscribe((newItems: QueryList<T>) =>
        this._itemsChanged(newItems.toArray()),
      );
    } else if (isSignal(_items)) {
      if (!injector && (typeof ngDevMode === 'undefined' || ngDevMode)) {
        throw new Error('ListKeyManager constructed with a signal must receive an injector');
      }

      this._effectRef = effect(() => this._itemsChanged(_items()), {injector});
    }
  }

  /**
   * Stream that emits any time the TAB key is pressed, so components can react
   * when focus is shifted off of the list.
   */
  readonly tabOut = new Subject<void>();

  /** Stream that emits whenever the active item of the list manager changes. */
  readonly change = new Subject<number>();

  /**
   * Sets the predicate function that determines which items should be skipped by the
   * list key manager.

View on GitHub (pinned to 0411926e7d)

Solutions

  1. Pass an injector: new ListKeyManager(itemsSignal, injector).
  2. Create the manager inside an injection context, e.g. in a field initializer of an Injectable/component or within runInInjectionContext.
  3. Use inject(Injector) in the class and forward it to the constructor.
  4. Only construct in dev-bypassing prod builds to hide it is not a fix — always supply the injector for signals.

Example fix

// before
this.keyManager = new ListKeyManager(this.itemsSignal);
// after
private injector = inject(Injector);
this.keyManager = new ListKeyManager(this.itemsSignal, this.injector);
Defensive patterns

Strategy: validation

Validate before calling

if (isSignal(items) && !injector) {
  throw new Error('Provide injector when constructing ListKeyManager with a signal');
}

Type guard

function isSignalLike<T>(v: unknown): v is Signal<T> {
  return typeof v === 'function' && (v as Signal<T>)[SIGNAL] !== undefined;
}

Try / catch

try {
  this.keyManager = new ListKeyManager(this.itemsSignal, this.injector);
} catch (e) {
  if ((e as Error).message.includes('must receive an injector')) {
    this.keyManager = new ListKeyManager(this.itemsSignal, inject(Injector));
  } else throw e;
}

Prevention

When it happens

Trigger: new ListKeyManager(mySignal) called outside an injection context (e.g. in a service constructed manually, a plain function, or a test) without passing {injector} or constructing within inject()/field initializer.

Common situations: Instantiating the key manager in a non-DI service helper; upgrading from QueryList/array items to a signal and forgetting the new injector parameter; running in tests with runInInjectionContext omitted.

Related errors


AI-assisted analysis of angular/components@0411926e7d (2026-08-31). Data as JSON: /api/errors/497e40c957b2aeb3. Report an issue: GitHub.