// with-username.feature.ts
import {
  ɵComponentDef as ComponentDef,
  ɵɵdirectiveInject as directiveInject,
} from '@angular/core';
import { select, Store } from '@ngrx/store';

export function withUsername(componentDef: ComponentDef<unknown>): void {
  const { factory, type } = componentDef;

  componentDef.factory = () => {
    const component = factory(type);
    const store = directiveInject(Store);
    component.username$ = store.pipe(select('username'));

    return component;
  };
}
The Ivy rendering engine in Angular brings a novel mechanism known as *component features*. Although these are not part of the public API in the initial Ivy release, Angular applies them internally to every component it processes. In essence, component features act as mixins for components, providing a way to augment, remove, or adjust component characteristics during the runtime. > Couldn’t we already accomplish this with base classes or custom decorators? Yes, but both strategies come with notable limitations. Traditional inheritance struggles because JavaScript’s object model permits only a single superclass, which tightly couples your class to the base. Modifications in that base class propagate to all subclasses, and integrating additional shared logic often requires intricate dependency injection and delegating control to helper objects. Custom decorators are equally problematic. They have yet to be ratified as an ECMAScript standard after many years. Their syntax and semantics remain in flux, and there’s a real chance they may never gain official inclusion, leaving them stranded in transpilers like TypeScript. Furthermore, custom decorators are not inherently tree-shakable, which is a significant drawback for bundle optimization. While Angular itself relies extensively on decorators, the Angular compiler converts them into runtime annotations and employs specialized techniques with what might be described as advanced internal magic to make them fully tree-shakable. > Couldn't a library just add its own compilation step, similar to what Angular does? That is a possibility, but it introduces additional package dependencies and necessitates the use of custom Angular CLI builders along with tailored WebPack configurations. ## Implementing Mixins Without the Traditional Tools Component features provide Angular’s native solution for mixins, sidestepping both inheritance and decorators. Since they are part of the core runtime, there is no requirement for custom CLI builders or WebPack configurations. Moreover, these features are automatically tree-shakable. > This seems almost too convenient. What is the catch? The primary limitation is that while component features are fully supported within the runtime, they are not exposed through any public API. To make them available to developers, the Angular team would simply need to add a features option to the Component decorator factory and implement a straightforward compilation step, akin to how they manage their own internal component features. ## The Reason for the Delay > Why hasn’t the Angular team made component features publicly available? There are two primary reasons for this. First, the initial Ivy release, Angular version 9, is predominantly focused on ensuring backward compatibility. The goal is to allow developers to migrate from the View Engine compiler and rendering engine to Ivy with minimal code changes. This priority leaves little room for adding substantial new functionality before achieving feature parity and maintaining stable compatibility. The timeline for Ivy completion has been lengthy for various reasons, which is a topic for another day. The second reason came to light when I proposed exposing component features to Minko Gechev. He expressed concern that making this internal API public would restrict the Angular team's ability to evolve and modify the framework. To fully grasp his reasoning, we need to examine the internal structure of component features in detail. ## Anatomy of a Component Feature The creation of a component feature begins with a factory function. This factory accepts custom parameters to tailor how the feature will behave. It acts as a higher-order function, meaning it returns another function—this returned function is the actual component feature that gets applied. > A *component feature* is defined as a function that accepts a *component definition* and executes side effects on it. The Angular runtime applies these features to component definitions exactly once during the component's lifecycle. Before we dive into a concrete example, let's clarify what component definitions are. ## Deciphering Component Definitions *Component definitions* represent the runtime representation of Angular component annotations. Within the Ivy architecture, these are attached as static properties on the component class. In Angular version 8, this was stored under the ngComponentDef static property. However, starting with Angular version 9, this location changed to a new static property named ɵcmp. The *ɵ* prefix denotes that this part of the API is *experimental* or unstable, while cmp abbreviates *component* (or more precisely, *component definition*). The shape of a component definition is formalized as ComponentDef<T>. This is a rich data structure containing numerous metadata properties that the Ivy runtime relies on. These include details like view encapsulation mode, the use of the OnPush change detection strategy, directive definitions accessible to the view, component selectors, and lifecycle hooks. For our purposes, the crucial metadata property is features, which is either null or holds an array of component features. The most valuable metadata property for building our own features is factory. This factory function can be invoked with the component type (the component class) to create a new instance. Additionally, lifecycle hooks within the component definition are advantageous for certain kinds of component features. ## A Practical Example: withUsername Let's illustrate with a simple scenario. Suppose our application relies on NgRx Store for state management. We can retrieve the current username from the store using the key 'username' . Many components require access to this username. A tedious approach would involve injecting the store everywhere and creating a new observable each time. Alternatively, we could wrap it in a user service and inject that everywhere. Instead, we define a straightforward component feature called withUsername to address this. *Listing 1: The username component feature.* It's important to understand that the feature itself doesn't construct the component instance or perform the injection. Its sole responsibility is to replace the original component factory with a new one in the component definition. Within this replacement factory, we first create a component instance by calling the original factory. Then, we proceed with the injection of the NgRx Store, extract the relevant state slice, and assign it to the observable username$ property on the newly created instance. Finally, the factory returns the component instance, which now has the username$ property populated.

Working with component features

As noted earlier in this article, component features are not part of Angular's public API. Had they been exposed, applying the username component feature would look something like the code in Listing 2.

// profile.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { withUsername } from './with-username.feature';

@Component({
  features: [
    withUsername,
  ],
  selector: '[appProfile]',
  template: `
    Username: {{username$ | async}}
  `,
})
export class ProfileComponent {
  username$: Observable<string>;
}

Listing 2. Applying the username component feature, assuming the Component decorator factory supported component features.

The appeal of mixins without inheritance is the ability to attach multiple traits to a single class of objects with ease. Based on Listing 2, it is fairly obvious that adding several component features is simply a matter of listing them inside the features option array.

One can only wonder what would become possible if Angular actually made this capability available to developers (pun intended).

Is it possible to use them right now?

Absolutely! There is, however, the usual warning that here be dragons. Since these parts of the Angular framework are meant to be experimental and internal, our code may not survive future Angular updates. We already saw that the static property that holds the component definition at runtime was renamed between Angular versions 8 and 9.

Below is a fairly straightforward class decorator that lets us leverage component features today, albeit with no stability guarantees across Angular releases.

// component-features.decorator.ts
import { Type, ɵNG_COMP_DEF } from '@angular/core';

import { ComponentDefFeatures } from './component-def-feature';

export function componentFeatures(features: ComponentDefFeatures) {
  return <T>(componentType: Type<T>) => {
    // At runtime, before bootstrap
    Promise.resolve().then(() => {
      const componentDef = componentType[ɵNG_COMP_DEF];

      if (componentDef === undefined) {
        throw new Error('Ivy is not enabled.');
      }

      componentDef.features = componentDef.features || [];

      // List features in component definition
      componentDef.features = [...componentDef.features, ...features];

      // Apply features to component definition
      features.forEach(feature => feature(componentDef));
    });
  };
}

Listing 3. A class decorator for component features.

The decorator shown in Listing 3 works with Ivy in Angular versions 8 and 9. Because Angular does not currently expose the ComponentDefFeature interface directly, the decorator relies on the interface and type definitions provided in Listing 4.

// component-def-feature.ts
import { ɵComponentDef as ComponentDef } from '@angular/core';

export interface ComponentDefFeature {
  <T>(componentDef: ComponentDef<T>): void;
  /**
   * Marks a feature as something that {@link InheritDefinitionFeature} will
   * execute during inheritance.
   *
   * NOTE: DO NOT SET IN ROOT OF MODULE! Doing so will result in
   * tree-shakers/bundlers identifying the change as a side effect, and the
   * feature will be included in every bundle.
   */
  ngInherit?: true;
}

export type ComponentDefFeatures = ReadonlyArray<ComponentDefFeature>;

Listing 4. The component feature interface and a collection type for features.

Returning to our profile component example, the custom decorator can be used as demonstrated in Listing 5.

// profile.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { componentFeatures } from './component-features.decorator';
import { withUsername } from './with-username.feature';

@Component({
  selector: '[appProfile]',
  template: `
    Username: {{username$ | async}}
  `,
})
@componentFeatures([
  withUsername,
])
export class ProfileComponent {
  username$: Observable<string>;
}

Listing 5. Using the custom component features decorator to apply a component feature.

Configuring component features with options

The username component feature currently assumes that the component declares an input property named username$. To make this configurable, we can turn the component feature into a component feature factory, as shown in Listing 6.

// with-username.feature.ts
import {
  ɵComponentDef as ComponentDef,
  ɵɵdirectiveInject as directiveInject,
} from '@angular/core';
import { select, Store } from '@ngrx/store';

import { ComponentDefFeature } from './component-def-feature.ts';

export function withUsername(inputName = 'username$'): ComponentDefFeature {
  return (componentDef: ComponentDef<unknown>): void => {
    const { factory, type } = componentDef;

    componentDef.factory = () => {
      const component = factory(type);
      const store = directiveInject(Store);
      component[inputName] = store.pipe(select('username'));

      return component;
    };
  };
}

Listing 6. A component feature factory.

For the sake of completeness, Listing 7 illustrates how to supply an option to a component feature factory.

// profile.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { componentFeatures } from './component-features.decorator';
import { withUsername } from './with-username.feature';

@Component({
  selector: '[appProfile]',
  template: `
    Username: {{profileName$ | async}}
  `,
})
@componentFeatures([
  withUsername('profileName$'),
])
export class ProfileComponent {
  profileName$: Observable<string>;
}

Listing 7. Supplying an option to a component feature factory.

The host features paradox

Here is the catch. If the Angular team is wary of making component features public, then they cannot expose renderComponent in its current form either. This function, used for bootstrapping components into the DOM, accepts an option named hostFeatures that takes an array of component features applied to the bootstrapped component. Angular itself includes a component feature called LifecycleHooksFeature, which enables lifecycle hooks such as ngOnInit for components bootstrapped via renderComponent.

// main.ts
import {
  ɵLifecycleHooksFeature as LifecycleHooksFeature,
  ɵrenderComponent as renderComponent,
} from '@angular/core';
import { AppComponent } from './app.component';

renderComponent(AppComponent, {
  hostFeatures: [
    LifecycleHooksFeature,
  ],
});

Bootstrapping a component that implements OnInit.

This leaves the Angular team with a few options: integrate lifecycle hook support directly into such components, expose component features through a public API, avoid publicly exposing renderComponent altogether (which would be a poor choice), or add yet another option to the Component decorator factory.

In my view, the Angular team will eventually need to expose component features publicly. I believe they should, as component features unlock powerful composition opportunities for Angular developers.

Minko's concern revolves around making the component definition public. Component features do allow advanced use cases such as supporting DSLs and non-HTML template languages, but the majority of use cases only require access to the component factory and dependency injection.

A possible compromise would be to pass only the component factory to custom component features and permit the use of directiveInject, as seen in our example. Exposing a narrower public API would keep internals hidden but would block a handful of more advanced scenarios.

Directive features

For completeness, it is worth mentioning that Ivy also introduces directive features. These behave almost identically to component features, with one key difference: the directive definition is stored on the static property ɵdir rather than ɵcmp, where dir stands for directive or more precisely directive definition.

From the examples we have covered, you can likely deduce how to create and apply directive features on your own.

Wrapping Up

Throughout this series, we have explored the concept of component features, the process of building them, their potential integration into Angular's public API, and practical ways to leverage them today through a custom decorator built on experimental Angular APIs.

Component features allow us to attach logic whose evaluation occurs at runtime. This represents a refreshing shift for a framework historically constrained by the metadata limitations imposed during ahead-of-time compilation.

Key Takeaways

Component decorators offer a mechanism for incorporating shared traits or repetitive glue logic without depending on class inheritance or custom decorators, except for the componentFeatures decorator we created here for demonstration. They introduce no additional package dependencies, require no custom WebPack configurations, and are fully tree-shakable.

Angular itself relies on these features internally to combine common behaviors. For those interested in deeper exploration, look for these in the Angular source:

  • ɵɵNgOnChangesFeature
  • ɵɵProvidersFeature
  • ɵɵInheritDefinitionFeature
  • ɵɵCopyDefinitionFeature

While component features were our primary focus, we also noted that directive features operate similarly and that host features for bootstrapped components might soon be exposed through Angular's public API.

To grasp component features fully, we examined the anatomy of component features, their factories, and briefly looked at component definitions.

Potential Applications for Component Features

I am optimistic that the Angular team will make component features publicly available. If so, they have the potential to fundamentally transform how we structure business logic within our components.

For inspiration, here are several use cases I believe component features could address:

  • Accessing route parameters, route data, and query parameters
  • Replacing container components, such as by integrating with NgRx Store, WebStorage, or other application state and persistence solutions
  • Managing a local store for UI-specific state
  • Observing component lifecycle events as observables
  • Transforming observables into event emitters
  • Advanced use, requiring Ivy instruction knowledge: listening to UI events like click and keypress
  • Handling subscription management and invoking markDirty

In fact, I've already built proof-of-concept features for several of these situations, available in my GitHub repository named ngx-ivy-features.

Limitations of Component Features

Like any potent technique, component features come with their own set of constraints.

Feature declarations are fixed at compile time; they cannot be altered based on runtime conditions. They are intended to be static metadata on a component. However, you could embed conditional logic within the features themselves to achieve dynamic behavior.

Each component or directive is restricted to a single list of feature declarations. This limitation suggests they may not function as drop-in replacements for higher-order components in React, though future Ivy updates might open other avenues.

The most significant current hurdle is that component features are not part of Angular's public API as of version 9. Nonetheless, they are supported by the Ivy runtime, allowing the adventurous to start using them now. We've demonstrated how in this article.

Let's put component features to the test and share our insights with the Angular team. Time to get experimental! ⚗️?‍?

Additional Reading

My Presentation on Component Features

In November 2019, I delivered a talk titled "Ivy's hidden features" at the ngPoland conference, and later reprised it at the Angular Online Event #3 in 2020.

Slides from my presentation "Ivy's hidden features/Ivy's best kept secret" at ngPoland 2019/Angular Online Event #3 2020. Open in new tab.

During the talk, I introduced component features and walked through simple examples of how they can be applied to solve common problems.

Experimental Component Features

I established the ngx-ivy-features GitHub repository to prototype and showcase a variety of component features. It includes router features, NgRx Store integrations, component lifecycle utilities, and LocalStorage implementations.

Gratitude

Several individuals have played a significant role in bringing this article to fruition, and I'd like to extend my thanks.

Peer Reviewers

This piece was made possible with the assistance of these exceptional people:

A Note of Thanks

I would also like to thank Minko Gechev from the Angular team for taking the time to discuss component features with me.