In Angular, how should a nested accordion component inject its parent accordion, and why does that lookup need skipSelf and optional?
answer
- the class is its own token
- closest match is yourself
- circular dependency error
- outermost instance has no parent
basics
~10 sUse inject(Accordion, {skipSelf: true, optional: true}). Without skipSelf the lookup finds the accordion on its own element and throws NG0200; without optional the outermost accordion, which has no parent, throws NG0201.
solid answer
~40 sComponents and directives are injectable by their class from their own element and from descendants, so an `Accordion` can inject `Accordion` to find an ancestor. A plain `inject(Accordion)` inside `Accordion` starts at its own element, finds **itself** while it is still being constructed, and throws `NG0200: Circular dependency detected`. `skipSelf: true` starts the search at the parent element, so the nearest *enclosing* accordion wins. The top-level accordion has no enclosing one, so the search runs on into the environment injectors, finds nothing and throws NG0201 — `optional: true` turns that into `null`, giving `parent: Accordion | null`. Items inside each accordion use a plain `inject(Accordion)`: the closest one is the right one, and nested accordions shadow outer ones naturally.
code
ts · 31 linesimport {Component, inject, input, signal} from '@angular/core';
@Component({
selector: 'app-accordion',
template: `<ng-content />`,
})
export class Accordion {
// nearest enclosing accordion, or null for the outermost one
readonly parent = inject(Accordion, {skipSelf: true, optional: true});
readonly depth: number = this.parent ? this.parent.depth + 1 : 0;
readonly openId = signal<string | null>(null);
}
@Component({
selector: 'app-accordion-item',
template: `
<button (click)="toggle()">{{ title() }}</button>
@if (accordion.openId() === title()) {
<ng-content />
}
`,
})
export class AccordionItem {
readonly title = input.required<string>();
// plain lookup: the closest accordion is the right one
protected accordion = inject(Accordion);
toggle() {
this.accordion.openId.update((id) => (id === this.title() ? null : this.title()));
}
}go deeper
Know that a component can inject an ancestor component by its class, and that skipSelf with optional is the usual way to find a parent of the same type.
Explain the NG0200 self-match, why the outermost instance needs optional, and why items use a plain lookup.
Compare skipSelf with host for recursive widgets, and design the parent-child registration and teardown so the relationship stays correct as panels are destroyed.
Decide whether recursive widgets couple to a concrete class or an abstract token, weighing flexibility for other teams against the simplicity of a single implementation.
## The scenario An FAQ page uses an accordion whose panels can contain another accordion. Two relationships have to be wired through DI: - each **item** must find the accordion that directly contains it, so that opening one item can close its siblings; - each **nested accordion** wants its **parent accordion**, for example to cascade "collapse all" or to compute its depth for indentation and ARIA levels. Angular makes this possible because **components and directives are themselves injectable by their class**, both from their own element and from descendants in the element tree. No provider needs to be declared for this. ## Why a plain lookup fails Inside `Accordion`, `inject(Accordion)` follows the default walk, which **starts at the requesting element**. The requesting element hosts the `Accordion` being constructed, so the first match is **itself**. Angular detects that it is resolving a token that is already being created and throws: ```text NG0200: Circular dependency detected for `Accordion`. ``` ## `skipSelf` moves the start `inject(Accordion, {skipSelf: true})` ignores the requesting element's injector and starts with its parent element. The first `Accordion` found on the way up is the nearest enclosing accordion — exactly the parent. For an item projected into a nested accordion, "nearest" is the nested one, as intended. ## `optional` handles the root case The outermost accordion has no enclosing accordion. With `skipSelf` alone, its element walk finds nothing, the lookup continues into the environment injectors — where no `Accordion` is provided — and reaches the `NullInjector`, which throws NG0201. `optional: true` turns that miss into `null`: ```ts readonly parent = inject(Accordion, {skipSelf: true, optional: true}); // Accordion | null readonly depth: number = this.parent ? this.parent.depth + 1 : 0; ``` ## Items need no modifier An `AccordionItem` uses plain `inject(Accordion)`: 1. It does not host an `Accordion` itself, so there is no self-match to skip. 2. The nearest enclosing accordion is found first, so nested accordions **shadow** outer ones for their own items. 3. Items outside any accordion are a programming error; the default NG0201 is the helpful outcome, so `optional` is deliberately **not** used. ## Why not `host` or `self`? | Option | Effect here | Verdict | |---|---|---| | `self` | Checks only the accordion's own element — finds itself, same NG0200 | Wrong | | `host` | Stops at the host of the declaring template; breaks when the nested accordion is declared inside another component's template | Too narrow for a general widget | | `skipSelf` + `optional` | Starts above itself, returns `null` at the top | Correct | `host` is sometimes suggested so that a nested accordion "cannot escape its container", but the boundary it enforces is the *template* that declares the accordion, not the accordion hierarchy. A nested accordion placed inside a reusable card component's template would stop at the card and get `null` even though an accordion encloses the card. ## Design notes - Keep the relationship read-only in the child: register with the parent (`parent?.registerChild(this)`) and unregister in `ngOnDestroy` or through `DestroyRef`, so the parent's list stays accurate as panels open and close. - If other component types can also act as "a parent accordion", inject an abstract token that `Accordion` provides with `useExisting`, instead of the concrete class, so alternative implementations can plug in. - The same `skipSelf` + `optional` shape appears in any recursive widget — trees, nested menus, nested form groups. ## Common interview traps - Saying the component must be added to `providers` to be injectable: components and directives on an element are resolvable by class without it. - Suggesting `self: true` to "stay local": for a component looking for its parent, local means itself, and it returns the same NG0200. - Forgetting teardown: a nested accordion that registers with its parent but never unregisters leaves the parent holding destroyed children after a panel's `@if` removes them.
- What error does inject(Accordion) inside Accordion raise without skipSelf, and why?NG0200, a circular dependency. The default walk starts at the requesting element, which hosts the Accordion being constructed; Angular sees it is resolving a token already under construction and throws rather than returning a half-built instance.
- Why should AccordionItem not use optional when injecting Accordion?An item outside any accordion cannot work, so the default NG0201 is the useful outcome: it fails at creation with a path pointing at the item. Making it optional would turn a template mistake into a null reference error on the first click.
saying these in an interview costs you the question
- Injecting a component's own class returns its parent automatically
- A component must be listed in providers to be injectable by class
- self: true finds the enclosing accordion
- skipSelf alone is safe for the outermost accordion
- Nested accordions' items are found by the outermost accordion first