skip to content

When would you compose Angular behaviours with `hostDirectives` instead of extending a base component or directive class, and how do the two differ?

level: seniorimportance: should knowfreq 35%

answer

  1. one base class, many directives
  2. union of everything versus opt-in
  3. separate instances, separate state
  4. overrides versus host-last order

basics

~10 s

Compose with hostDirectives to attach several independent behaviours, each with its own instance, state and opt-in API; inherit when a component is a specialised version of one base and should share its whole API.

solid answer

~50 s

Extending a component or directive class gives the child the union of the base's inputs, outputs, host bindings and lifecycle methods, and its fields and methods, all in one instance. That suits a genuine specialisation, like a `CustomListbox` extending `ListboxBase`. But TypeScript allows one base class, every base input becomes part of the child's API whether wanted or not, and behaviours mixed into one class share state and names. `hostDirectives` composes any number of standalone directives, each a separate instance with its own state, dependency injection and tests; only the inputs and outputs you list, optionally aliased, join the component's API; and the component's host bindings run last, so it can override theirs. So for orthogonal, reusable behaviours such as a tooltip, a focus ring and a disabled state on a menu item, compose; for a family of components that really are variations of one thing, inherit.

go deeper

for a junior

Recall that a class can extend only one base but can compose several directives, and that composed inputs are hidden unless listed.

for a middle

Contrast the resulting API, state and instance model of inheritance and hostDirectives on a concrete component.

for a senior

Choose by relationship, is-a versus has-behaviour, and justify the choice in terms of API control, testability and name clashes.

for a principal

Set a component-library policy: base classes for true families, host directives for cross-cutting behaviours, with consistent aliases across the library.

## Two ways to reuse behaviour Angular components and directives are TypeScript classes, so they can reuse code through **inheritance**. Since the directive composition API arrived in v15, they can also reuse it through **composition** with `hostDirectives`. Both put shared behaviour onto a component's host element; they differ in what the resulting component's API and state look like. ## What inheritance gives you When a component extends another component or directive, it inherits some of the base decorator's metadata and all decorated members: - the **union** of every ancestor's inputs, outputs and host bindings, plus its own; - inherited **lifecycle methods** and ordinary fields and methods; - dependencies the base obtains with `inject()` in field initialisers, without forwarding anything to `super`. It is **one instance**: base and child members live on the same object, share state and share a namespace. ## What composition gives you `hostDirectives: [Tooltip, FocusRing, DisabledState]` on a menu item gives: - **several independent instances** on one element, each with its own state, injected services and lifecycle; - an **opt-in API**: none of their inputs or outputs is exposed unless listed, and each can be aliased; - **host-last ordering**: the component's own host bindings are applied after the directives' ones, so it can override them; - behaviours that remain **standalone directives**, usable directly in templates and testable alone. ## Side by side | Aspect | Extending a base class | `hostDirectives` | | --- | --- | --- | | How many sources | One base class chain | Any number of directives | | Public API | Every inherited input and output | Only listed bindings, optionally aliased | | State | Shared in one instance | Separate per directive instance | | Name clashes | Possible between base and child members | Isolated; only exposed aliases can clash | | Access from the component | `this.member` | `inject(Directive)` | | Can be applied at runtime | No | No, fixed at compile time | | Best fit | A specialisation of one thing | Orthogonal, reusable behaviours | ## Choosing Ask what the relationship is: 1. **"Is a"**: a `CustomListbox` is a listbox with extra features. It needs the base's inputs and internal methods, and exposing the whole base API is correct. Inherit. 2. **"Has the behaviour of"**: a menu item has a tooltip, a focus ring and a disabled state. Those are unrelated to each other and to menu semantics. Compose. 3. **Several behaviours needed at once**: inheritance forces an artificial chain (`FocusRingBase extends TooltipBase`), which composition avoids. 4. **API control matters**, as in a component library: composition keeps the surface deliberate, while inheritance leaks every base input. The two also combine: a base component can list `hostDirectives`, and a component that extends it keeps that behaviour. What to avoid is using inheritance purely to share host bindings across unrelated components; that was the main workaround before the composition API existed. ## A refactoring example A library once had `TooltipBase`, with a `tooltip` input and mouse listeners, and every interactive component extended it. Problems accumulated: components that also needed a `DisabledBase` could not extend both, every component exposed `tooltip` even where a hint made no sense, and a component's own `show()` method collided with the base's. Moving to composition: 1. Turn `TooltipBase` into a standalone `Tooltip` directive with no selector. 2. Remove `extends TooltipBase` from each component and add `hostDirectives: [{ directive: Tooltip, inputs: ['text: tooltip'] }]` where a tooltip is wanted, keeping the old public name through the alias. 3. Replace `this.show()` calls that meant the tooltip with calls on an injected `Tooltip`. 4. Do the same for `DisabledBase`, now composable alongside. Templates using `tooltip="..."` keep working, and components that should not have a tooltip simply do not compose it. ## Costs of composition - **More instances**: each host directive is a separate object with its own injector participation, which is negligible for a few per component but worth noting in very large lists. - **Indirection**: the component reaches a behaviour's state through `inject()`, not `this`. - **Alias discipline**: in v22, shared directives reached through several paths merge into one instance, and inconsistent aliases for the same binding fail with NG8024.

  • Why is inheriting from a base directive a poor way to share a tooltip across buttons, links and menu items?
    Each of those components would have to extend the tooltip base, which uses up their single base class, puts every tooltip input on their API, and mixes the tooltip's state into each component. Composing a standalone `Tooltip` directive keeps it separate, lets each component expose only what it wants, and leaves the base class free.
  • Can a component both extend a base class and use `hostDirectives`?
    Yes. The two mechanisms are independent: the class inherits the base's members and metadata, and the host directives are applied to its element as usual. A common shape is a base class for the family's shared logic and host directives for cross-cutting behaviours such as focus handling.

Inheritance is buying a car model that is a variant of another: you get the whole base car, every control included. Composition is fitting separate accessories to your car, a roof rack, a tow bar, a dash camera, each working on its own and each adding only the controls you choose to wire to the dashboard.

saying these in an interview costs you the question

  • hostDirectives exposes all of the composed directives' inputs, just like inheritance.
  • Inheritance creates a separate instance for the base class alongside the child.
  • Host directives share one instance and one state with the host component.
  • Composition can be applied conditionally at runtime, unlike inheritance.
  • A component can extend several base directive classes to combine behaviours.