For years, Angular has been highly regarded as the javascript framework for enterprise development, not because of its excellent developer experience but because of its ease of extensibility and maintainability. It is very easy to walk into an Angular shop with Angular experience and immediately start contributing because every Angular application, at its core, is still an Angular application. That being said, over the past year, the Angular team has been working to make the developer experience one of the framework's key focal points, and one of those areas has been to make dynamic component loading easier to implement.
Dynamic Components
Most people argue that Angular 14 was the beginning of the #AngularRevolution, but it started six months earlier with 13. In Angular 13, we started getting the "Ivy Everywhere" by removing the old View Engine. With Ivy, we were able to convert our old Dynamic components from this:
@Component({
template: `
<button (click)="showModal()">Show Modal</ng-template>
<ng-template #payModal></ng-template>
`,
})
export class PaymentPage {
constructor(private readonly factory: ComponentFactoryResolver) {}
@ViewChild('payModal', { static: true, read: ViewContainerRef })
public modalContainer: ViewContainerRef;
showModal() {
this.modalContainer.clear();
const component = this.factory.resolveComponentFactory(PaymentModal);
this.modalContainer.createComponent<PaymentModal>(component);
}
}
to this:
@Component({
template: `
<button (click)="showModal()">Show Modal</ng-template>
<ng-template #payModal></ng-template>
`,
})
export class PaymentPage {
@ViewChild('payModal', { static: true, read: ViewContainerRef })
public modalContainer: ViewContainerRef;
showModal() {
this.modalContainer.clear();
this.modalContainer.createComponent<PaymentModal>(PaymentModal);
}
}
Even though the Angular team drastically reduced the amount of boilerplate needed in Angular 13, there is still some boilerplate when creating a component dynamically. Sometimes, hammering down the necessary boilerplate for new developers or developers who have yet to use this pattern is difficult.
For starters, you have to remember to clear the contents inside the ng-template. If there was a component rendered there from a previous call, the contents would still be there if you created another component in that space. The replacement of the createComponentFactory with just createComponent is a win, but it can still prove difficult trying to teach an inquisitive developer what's going on here other than the high-level, “you're creating a component in this spot.” The Ivy renderer is what's doing the heavy lifting behind the scenes.
Even still, the Angular team improved the experience in Angular 17…
Deferred Components
I'm confident that I'm not the only person who has thought, "deferred components seem a lot like dynamic components." And in all actuality, many of the same things are happening under the hood. One of the DX improvements that were made here was cutting down the boilerplate even further, removing the need for a ViewContainerRef and just allowing the renderer to do its thing.
@Component({
imports: [PaymentModalComponent],
template: `
<button #showModal>Show Modal</button>
@defer (on interaction(showModal)) {
<payment-modal />
}
`,
})
export class PaymentPage {}
The funky syntax in the template is all part of the new control flow. With it, we can tell the renderer to defer loading the contents of the @defer block until a user interacts with the named template variable. There are many different declarative triggers that the @defer takes: "interaction," "idle," "timer," "viewport," "hover," and "immediate." Each of those arguments does precisely what it says and provides more flexibility than you could do with dynamic components.
Let's take the example I provided above with a modal that will appear as soon as a user clicks the "show me" button. I could modify the defer argument to @defer(on interaction(showModal); prefetch on idle) to make it prefetch data for that modal before a user interacts with the button. You couldn't do this quickly before with dynamic components. Along with that, I could combine any combination of these triggers to make pretty fun conditions.
@Component({
imports: [PaymentModalComponent],
template: `
<button #showModal>Show Modal</button>
<button (click)="toggleModals()">Toggle Modals</button>
@defer (on interaction(showModal); when openModals()) {
<payment-modal />
}
`,
})
export class PaymentPage {
public openModals = signal(false);
toggleModals() {
this.openModals.update((val) => !val);
}
}
These triggers can get extremely powerful as there is nearly no limit to what you can trigger on; however, you can't use the same trigger twice in the same defer.
Deferred Component Caveats
An astute reader may have noticed that in the Angular 17 example of the deferred loading there is an imports list containing the PaymentModalComponent. As of this article, the consuming component must import the deferred component. This differs from the dynamic component examples, where we are programmatically creating a component. In the deferred component example, the component context is already aware of the injection context so we don't need to do all of that in the component class.
With dynamic components, you could create logical chains similar to what you could do with multiple conditions. The difference with the deferred flow is that using various triggers will always be read as "OR" instead of "AND." I could modify my example to trigger when a user interacts with the button OR after a timer expires after 5 seconds; however, I couldn't create a condition where the user interacts with the button, and the page has idled for 5 seconds just in the template. I could, however, create a more imperative @defer(when …) that combined different logical situations that could be combined with a signal and updated when that value changed.
Another caveat, albeit a small one, is that the component to be deferred needs to be a standalone component. It's also important to note that if you're exporting anything else from the file (e.g. consts, functions, interfaces), you may eagerly load those references elsewhere and you will no longer be lazy-loading the component.
The caveats may not be too much of a hassle if you enjoy the DX. The DX is a huge improvement and will allow junior devs as well as experienced devs to use a much more performant workflow (when compared to using flags with data in the template) with less complication.