Master the new APIs that make dynamic component creation cleaner, type-safe, and developer-friendly
Ever struggled with creating dynamic components in Angular and wished there was a cleaner, more intuitive way? 🤔
If you've been working with Angular's dynamic components using ComponentFactory or ViewContainerRef, you know the pain of verbose code, complex event handling, and the constant worry about memory leaks. Well, Angular 20 just dropped some absolute game-changers that will make you rethink how you approach dynamic components entirely.
⚠️ Important Scope Note
The APIs inputBinding, outputBinding, and twoWayBinding are not general-purpose replacements for @Input() or @Output().
They are only supported when creating components dynamically using createComponent() and are intended for runtime component composition, not template-based bindings.
What you'll learn by the end of this article:
- How to use Angular 20's new
inputBinding(),outputBinding(), andtwoWayBinding()APIs - Best practices for memory management and cleanup
- Complete unit testing strategies
- Performance optimization techniques
- Real-world implementation examples with clean, maintainable code
Ready to level up your Angular game? Let's dive in! 👇
The Old Way vs. The New Way: A Quick Reality Check
Before we jump into the good stuff, let's be honest about what we used to deal with:
// The old, painful way 😫
const componentRef = this.viewContainer.createComponent(MyComponent);
componentRef.instance.someInput = 'value';
componentRef.instance.someOutput.subscribe(data => {
// Handle output
});
// Don't forget cleanup... if you remember 🤷♂️
ngOnDestroy() {
// Manual subscription cleanup
}
Sound familiar? We've all been there—wrestling with subscriptions, forgetting cleanup, and ending up with memory leaks that haunt our apps.
💬 Quick question for you: How many times have you forgotten to unsubscribe from dynamic component outputs? Be honest in the comments!
Meet the New Heroes: inputBinding(), outputBinding(), and twoWayBinding()
Angular 20 introduces three powerful methods that transform how we work with dynamic components. Let's break them down:
1. inputBinding() - Clean Input Handling
import { Component, ViewContainerRef, inject } from '@angular/core';
import { inputBinding } from '@angular/core';
@Component({
selector: 'app-dynamic-host',
template: `<div #dynamicContainer></div>`
})
export class DynamicHostComponent {
private viewContainer = inject(ViewContainerRef);
addDynamicComponent() {
this.viewContainer.createComponent(UserCardComponent, {
bindings: [
inputBinding('userName', 'John Doe'),
],
directives: [ ... ]
});
}
}
2. outputBinding() - Effortless Event Handling
addDynamicComponentWithEvents() {
this.viewContainer.createComponent(UserCardComponent, {
bindings: [
outputBinding<boolean>('close', (userData) => {
console.log('User clicked:', userData);
this.handleUserSelection(userData);
}),
],
directives: [ ... ]
});
}
3. twoWayBinding() - The Holy Grail
This is where it gets really exciting. Two-way binding with dynamic components used to be a nightmare. Not anymore:
setupTwoWayBinding() {
this.vcr.createComponent(AppWarningComponent, {
bindings: [
twoWayBinding('isExpanded', this.isExpanded),
],
directives: [ ... ]
});
}
Pretty clean, right?
Unit Testing Your Dynamic Components
Testing dynamic components can be tricky, but Angular 20's new APIs make it much more straightforward:
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent, ChartWidgetComponent, CounterWidgetComponent],
imports: [CommonModule]
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
});
describe('Dynamic Widget Creation', () => {
it('should create chart widget with correct inputs', () => {
// Act
component.addWidget('chart');
fixture.detectChanges();
// Assert
const chartComponent = fixture.debugElement.query(
By.directive(ChartWidgetComponent)
);
expect(chartComponent).toBeTruthy();
expect(chartComponent.componentInstance.title).toBe('Sales Chart');
expect(chartComponent.componentInstance.chartType).toBe('line');
});
it('should handle widget events correctly', fakeAsync(() => {
// Arrange
spyOn(component, 'removeWidget');
// Act
component.addWidget('chart');
fixture.detectChanges();
const chartComponent = fixture.debugElement.query(
By.directive(ChartWidgetComponent)
);
// Simulate delete event
chartComponent.componentInstance.deleteRequested.emit();
tick();
// Assert
expect(component.removeWidget).toHaveBeenCalled();
}));
it('should handle two-way binding for counter widget', fakeAsync(() => {
// Arrange
spyOn(component, 'saveWidgetState');
// Act
component.addWidget('counter');
fixture.detectChanges();
const counterComponent = fixture.debugElement.query(
By.directive(CounterWidgetComponent)
);
// Simulate count change
counterComponent.componentInstance.count = 5;
counterComponent.componentInstance.countChange.emit(5);
tick();
// Assert
expect(component.saveWidgetState).toHaveBeenCalledWith(
jasmine.any(String),
{ count: 5 }
);
}));
});
describe('Memory Management', () => {
it('should clean up subscriptions on destroy', () => {
// Arrange
component.addWidget('chart');
component.addWidget('counter');
const subscriptionCount = (component as any).subscriptions.size;
expect(subscriptionCount).toBeGreaterThan(0);
// Act
component.ngOnDestroy();
// Assert
expect((component as any).subscriptions.size).toBe(0);
expect((component as any).activeComponents.length).toBe(0);
});
});
});
Testing tip: Always test both the happy path and the cleanup! Memory leaks in tests can be just as problematic as in production.
Best Practices & Memory Management
1. Always Clean Up Your Subscriptions
export class DynamicComponentManager implements OnDestroy {
private cleanup: (() => void)[] = [];
addComponent() {
const componentRef = createComponent(MyComponent, {
environmentInjector: this.injector,
bindings: [
outputBinding('someEvent', this.handleEvent),
],
});
}
}
2. Use WeakMap for Component Metadata
export class AdvancedDynamicManager {
private componentMetadata =
new WeakMap<ComponentRef<any>, ComponentMetaData>();
addComponent() {
const componentRef = this.createComponent();
this.componentMetadata.set(componentRef, {
createdAt: Date.now(),
type: 'chart',
subscriptions: [],
});
}
removeComponent(componentRef: ComponentRef<any>) {
const meta = this.componentMetadata.get(componentRef);
// Manual cleanup is still required
meta?.subscriptions.forEach(sub => sub.unsubscribe());
componentRef.destroy();
// WeakMap entry becomes eligible for GC automatically
}
}
3. Implement Change Detection Optimization
export class OptimizedDynamicHost {
addComponent() {
const componentRef =
this.viewContainer.createComponent(MyComponent);
// Detach from automatic change detection
componentRef.changeDetectorRef.detach();
// Set input explicitly after creation
componentRef.setInput('data', this.data);
// Manually trigger change detection when needed
componentRef.changeDetectorRef.detectChanges();
}
}
4. Measure Dynamic Component Creation Performance
export class PerformanceAwareDynamic {
private performanceMetrics = new Map<string, number>();
addComponent(type: string) {
const startTime = performance.now();
const componentRef = this.createComponent(type);
const endTime = performance.now();
const duration = endTime - startTime;
this.performanceMetrics.set(type, duration);
if (duration > 100) {
console.warn(
`Slow component creation for ${type}: ${duration.toFixed(2)}ms`
);
}
}
}
Quick question: Are you currently monitoring your dynamic component performance? Drop a comment and let me know what tools you use! 💬
Bonus Tips
1. Type Safety with Generics
function createTypedComponent<T>(
viewContainer: ViewContainerRef,
componentType: Type<T>,
inputs: Partial<Record<keyof T, unknown>> = {}
): ComponentRef<T> {
const componentRef = viewContainer.createComponent(componentType);
(Object.entries(inputs) as [keyof T, unknown][]).forEach(
([key, value]) => {
componentRef.setInput(key as string, value);
}
);
return componentRef;
}
// Usage with full type safety
const chartRef = createTypedComponent(
this.viewContainer,
ChartComponent,
{
title: 'My Chart',
data: chartData,
showLegend: true,
}
);
2. Component Factory Service
createComponent<T>(
name: string,
viewContainer: ViewContainerRef,
inputs?: Partial<T>
): ComponentRef<T> {
const componentRef = viewContainer.createComponent(this.registry.get(name) as Type<T>);
if (inputs) {
(Object.entries(inputs) as [keyof T, unknown][]).forEach(([key, value]) => {
componentRef.setInput(key as string, value);
});
}
return componentRef;
}
3. Error Boundary for Dynamic Components
export class SafeDynamicHost {
addComponentSafely(componentType: Type<any>) {
try {
const componentRef = this.viewContainer.createComponent(componentType);
// Wrap in error handling
const originalNgOnInit = componentRef.instance.ngOnInit;
componentRef.instance.ngOnInit = () => {
try {
originalNgOnInit?.call(componentRef.instance);
} catch (error) {
console.error('Dynamic component initialization failed:', error);
this.handleComponentError(componentRef, error);
}
};
return componentRef;
} catch (error) {
console.error('Failed to create dynamic component:', error);
return null;
}
}
}
Recap: Your Dynamic Component Toolkit 📚
Let's wrap up what we've covered:
Key Angular 20 Features:
-
inputBinding()- Clean, type-safe input handling -
outputBinding()- Simplified event subscription with automatic cleanup options -
twoWayBinding()- Effortless two-way data binding for dynamic components
Best Practices Checklist:
- ✅ Always clean up subscriptions in
ngOnDestroy - ✅ Use WeakMap for component metadata
- ✅ Monitor performance of dynamic component creation
- ✅ Implement error boundaries for robust apps
- ✅ Leverage TypeScript generics for type safety
- ✅ Test both functionality and memory management
Memory Management Essentials:
- Store cleanup functions and execute them on destroy
- Use
ChangeDetectorRef.detach()for performance optimization - Monitor component lifecycle with Angular DevTools
- Implement proper error handling to prevent memory leaks
When Should You Use These APIs?
Use inputBinding, outputBinding, and twoWayBinding only if:
You are creating components dynamically
You cannot use Angular templates
You need signal-based bindings at runtimeDo NOT use them if:
You are building standard Angular components
You are using template bindings ([input], (output))
You expect them to replace @Input() or @Output()
Your Turn! 👇
💬 What did you think? Have you already started experimenting with Angular 20's dynamic component features? I'd love to hear about your experience! Drop a comment below and share:
- What's your biggest pain point with dynamic components?
- Have you tried these new APIs yet?
- What other Angular 20 features are you excited about?
👏 Found this helpful? If this article saved you some debugging time or sparked new ideas, smash that clap button! It helps other developers discover these game-changing features too.
📬 Want more Angular insights like this? I share practical Angular tips, advanced techniques, and the latest framework updates every week. Follow me here on Medium and never miss out on the good stuff!
🚀 Take Action:
- Try implementing one of these examples in your current project
- Refactor an existing dynamic component using the new APIs
- Share this article with your team - they'll thank you later!
- Star the GitHub repository with the complete examples (link in my bio)
Keep coding, keep learning, and remember - the best way to master these features is to get your hands dirty with code! 💪
🎯 Your Turn, Devs!
👀 Did this article spark new ideas or help solve a real problem?
💬 I'd love to hear about it!
✅ Are you already using this technique in your Angular or frontend project?
🧠 Got questions, doubts, or your own twist on the approach?
Drop them in the comments below — let’s learn together!
🙌 Let’s Grow Together!
If this article added value to your dev journey:
🔁 Share it with your team, tech friends, or community — you never know who might need it right now.
📌 Save it for later and revisit as a quick reference.
🚀 Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
🎉 If you found this article valuable:
- Leave a 👏 Clap
- Drop a 💬 Comment
- Hit 🔔 Follow for more weekly frontend insights
Let’s build cleaner, faster, and smarter web apps — together.
Stay tuned for more Angular tips, patterns, and performance tricks! 🧪🧠🚀
Top comments (3)
May I ask which AI tool was used to generate this article? Because this all is complete nonsense, suggested API does not exist in v20 or v21. Yes, there are inputBinding/outputBinding/twoWayBinding functions, but they can only be used in createComponent calls like this one:
Thank you for your detailed feedback! You are correct that the previous examples were inaccurate. When I originally wrote this article, these APIs (inputBinding, outputBinding, twoWayBinding) were still in developer preview, so some usage patterns were different from the stable release.
I’ve now updated the article to reflect the current, stable Angular v20+ behavior:
Bindings are used only at component creation time via createComponent()
Runtime updates now use setInput() and proper subscription handling
Change detection, type safety, and cleanup have been clarified
I really appreciate your input — it helped improve the accuracy and usefulness of the article for everyone.
It still hallucinates. There was no "technical preview" for this functionality - function params were always the same (original PR). Regarding type safety: there's no type safety with these functions, which is the problem. You can create your own wrapper, like in "Type Safety with Generics" example, but there's no built-in type check. As a result, you can map whatever you want to any component field, and then just hope component inputs/outputs won't change in the future
Let me point out some more mismatches, so you can review them (I hope) and update your article a bit more