- 🧩
ngOnDestroy()won't auto-trigger for components created withComponentPortal. - 💾 Forgetting
ref.destroy()can lead to severe memory leaks in Angular apps. - ⚠️ Overlays using
ComponentPortalremove DOM but not the component instance. - 🔍 DevTools memory profiling reveals retained components not being destroyed.
- 🛠️ Wrapping ComponentPortal logic in services ensures safer lifecycle management.
The Lifecycle Gap in Angular Portals
If you use Angular's CDK and ComponentPortal to show components, you might have noticed a problem. The ngOnDestroy() lifecycle hook doesn’t get called when you expect it to. This can cause memory leaks and performance issues. Nobody wants that. This article tells you why this happens. It also explains how Angular manages component lifecycles with portals, and how you can make sure your components get cleaned up.
Understanding the Role of ngOnDestroy in Angular
Angular gives you lifecycle hooks. These let you connect to certain points in a component's life: when it is made, updated, or removed. ngOnDestroy() is one of the most important hooks.
Angular calls this method when it is about to remove a component or directive from the DOM. It also calls it when it destroys the component's instance. People commonly use it to:
- ✅ Stop listening to observables. This prevents endless
next()calls. - ✅ Take away event listeners from the browser or DOM.
- ✅ Clear
setIntervalorsetTimeouttimers. - ✅ Run shutdown steps for services or other libraries.
- ✅ Get rid of large memory items, like canvas elements or maps.
Normally, Angular does this on its own. It happens when you use structural directives like *ngIf or routing. But things are very different when you create a component outside of Angular’s normal component structure. This happens with ComponentPortal, for example.
What is ComponentPortal?
ComponentPortal is a class from the Angular Component Dev Kit (CDK). It helps you use methods where you need to show components programmatically. You can attach a component to a PortalOutlet when your app is running. This means your code decides what to show and where to show it at that moment.
Here are four common ways to use it:
- Modals – Dialogs that look different based on what a user types.
- Tooltips and Popovers – UI items that float or appear next to other controls.
- Dropdowns and Menus – Interaction items with many parts.
- Dashboards and Widgets – Screens you can build with movable sections while the app runs.
Basic Code Example
const portal = new ComponentPortal(MyComponent);
const ref = portalOutlet.attach(portal);
When you run this code:
MyComponentgets created.- It goes into the
PortalOutletyou gave it. This could be an overlay or a container. - You then get a
ComponentRef<MyComponent>. This points to the component that is now active.
At this point, the component shows up. But it is not clear that nothing will remove it if you forget ref.destroy().
Why ngOnDestroy() Doesn’t Fire Automatically
In Angular’s normal way of working, the framework creates and removes components. It does this through routers, container components, or structural directives like *ngIf. In these cases, Angular follows the component's lifecycle. And it makes sure everything is taken down, including calling ngOnDestroy().
But when you use ComponentPortal, the framework puts most of the cleanup work on you, the developer:
- Angular makes the component instance by hand using the injector.
- The component is not connected to Angular’s usual change detection and lifecycle system.
- Lifecycle hooks like
ngOnDestroy()do NOT get connected by themselves when the component leaves the DOM.
So, if you do not clearly call ref.destroy() on the ComponentRef, the ngOnDestroy() method of your component will never run. This causes your app to hold onto resources.
Component Lifecycles and Component Showing Realities
When you use ComponentPortal to make and put in a component, Angular gives you a ComponentRef<T>. This object is very useful and lets you:
- An actual instance of the component:
ref.instance - The host element in the DOM:
ref.location.nativeElement - Access to change detection:
ref.changeDetectorRef - Manual lifecycle controls:
ref.destroy()
Proper Lifecycle Flow with ComponentPortal
const portal = new ComponentPortal(MyComponent);
const ref = portalOutlet.attach(portal); // Component initialized and rendered
// ...
ref.destroy(); // Properly triggers ngOnDestroy()
If you skip ref.destroy(), it means:
- Subscriptions stay active.
- Your component still uses memory.
- DOM elements might be gone, but the component instances can still be there and leak memory.
Why Overlays and ComponentPortal Need Clear Destruction
The Angular CDK Overlay system often shows this problem. This is a common way to use ComponentPortal. When an overlay closes, it usually takes away the DOM node. But it does not take away the Angular component itself that you put there.
This difference can cause several unexpected issues:
- 🔄 Subscriptions in components might keep sending out data. This can cause bugs.
- 📌 Event listeners, like
window.addEventListener(), do not get removed. - 💥 You might see a 30–50% rise in memory use over time if overlays are not managed.
- 🧪 Components that are no longer linked can cause bugs that are hard to find.
Example Use Case: Tooltip Leak
const portal = new ComponentPortal(TooltipComponent);
const ref = overlayRef.attach(portal);
// Overlay gets removed visually
overlayRef.detach(); // Just removes the DOM, NOT the component
// TooltipComponent is still alive in JS memory
This way of doing things is common, but it is risky. Developers think removing the DOM node means the component is fully gone. But this is not true for Angular's CDK.
Fix: Always Call .destroy() on the ComponentRef
No matter if you use overlays, containers, or dashboard widgets, always keep the reference to your ComponentRef. And make sure you get rid of it carefully.
Correct Pattern
const portal = new ComponentPortal(MyComponent);
const ref = portalOutlet.attach(portal);
// Later during teardown
ref.destroy(); // ngOnDestroy() will be called here
This one method call makes sure:
- The component's teardown steps run.
- Observables, listeners, and timers are removed.
- Angular's garbage collector can free up memory.
Centralized Lifecycle Management
People make mistakes. And it is easy to forget to destroy components when you work with many UIs. What is the best way to handle this? Put all this logic in one place.
Sample Service for Portal Lifecycle Management
@Injectable({ providedIn: 'root' })
export class PortalService {
private refs: ComponentRef<any>[] = [];
attachComponent<T>(component: Type<T>, outlet: PortalOutlet): ComponentRef<T> {
const portal = new ComponentPortal(component);
const ref = outlet.attach(portal);
this.refs.push(ref);
return ref;
}
destroyAll() {
this.refs.forEach(ref => ref.destroy());
this.refs = [];
}
}
This service does these things:
- ✅ It attaches and keeps track of components.
- ✅ It destroys many components during teardown.
- ✅ It makes sure component lifecycles are the same throughout the app.
Apply destroyAll() during navigation, modal closing, tab switching, or other teardown events.
Safer Alternatives to ComponentPortal
While ComponentPortal is useful, sometimes you do not need it. Think about if simpler Angular tools can fix your problem without making things too hard.
Substitutes:
- Use routing: Let Angular handle views and their lifecycles.
- Use
*ngIf/*ngFor: Show or hide content as needed. Angular will handle their lifecycles. - Use Angular Material modules: Their dialogs, overlays, and layout components know about lifecycles. They use
ComponentPortalmethods behind the scenes. - Use
ngComponentOutlet: To load components in templates as needed. This includes binding and destroy lifecycles.
Only use ComponentPortal when:
- Your components need to be separate from their surroundings.
- You must have full control over how things look (like absolute positioning or overlays).
- You are putting together parts of a screen as needed (like dashboards with widgets).
Testing ngOnDestroy() and Memory Cleanup
Never assume components are cleaned up the right way. Always test them. Here are some ways to do that:
Log from ngOnDestroy
ngOnDestroy() {
console.log('Component destroyed');
}
Check browser logs when you do things like navigating or closing a modal. This will confirm it.
Use Chrome DevTools for Memory Profiling
- Open Performance tab
- Record a session that includes component creation and teardown
- Inspect the memory/references panel
- Search for still-alive instances of your component
If you see components still there, it means you forgot to call destroy().
Unit Testing Example
it('should destroy dynamic component on teardown', () => {
const portal = new ComponentPortal(MyComponent);
const ref = outlet.attach(portal);
spyOn(ref.instance, 'ngOnDestroy' as any);
ref.destroy();
expect(ref.instance.ngOnDestroy).toHaveBeenCalled();
});
You can also automate memory checks. Use headless testing tools like Cypress with performance plugins, or Puppeteer benchmarks.
Risks of Ignoring the Issue
If you do not clean up components that are made as needed, you face big, ongoing problems:
- ❌ Memory leaks, especially in apps that run for a long time.
- ❌ Event handlers that go off when you do not expect them, like during navigation or after modals close.
- ❌ More component error logs and odd bugs that are hard to fix.
- ❌ Bigger app size and slower performance, especially on less powerful devices.
Some reports and tests show that memory leaks from portals you do not destroy can make Angular apps that use many dashboards use 20–70% more memory.
Real-World Use Cases & Challenges
Here are some real scenarios where proper ComponentPortal handling makes a difference:
Dashboards
- Many components loaded based on what the user sets up.
- Lifecycle steps that are hard to guess.
- Users might not "unload" components unless you control it with code.
Modal-Based Interfaces
- Opening and closing modals quickly makes component lifetimes jump.
- Some modals can hold 3–5 components.
- Leaking these makes memory use much, much higher.
Marketing Widgets & Custom Forms
- Added to the DOM as needed through a main setup or tools that change content for users.
- Often removed from the view but never destroyed. This means the DOM node stays there without you being able to control it.
In each case, the answer is: Keep the ComponentRef. And make sure you destroy it.
Best Practices Checklist
- ✅ Always keep and manage your
ComponentRef. - ✅ Always call
ref.destroy()during teardown. - ✅ Put portal logic and clean-up inside special services.
- ✅ Do not use
ComponentPortalif normal Angular ways work. - ✅ Test lifecycles and use DevTools to check if
ngOnDestroy()runs. - ✅ Look at memory use when you test overlays and widgets.
- ✅ Do not trust what you see (DOM removal) to mean the component is gone.
Further Reading and Resources
- Angular Lifecycle Hooks — Official Docs
- Angular CDK Portal Module Overview
- ComponentPortal API Documentation
- Portal Scenarios Inside Angular CDK
- Chrome DevTools Memory Profiler Guide
Build With Confidence
At Devsolus, we want to help Angular developers avoid tricky problems like this. Then you can build good, fast apps without worrying about hidden memory leaks. Once you better understand how Angular handles lifecycles for things created on the fly, and use some helpful methods, you will be able to handle your overlays and components well.
Citations
Angular CDK Docs. (n.d.). Retrieved from https://angular.io/cdk/portal/overview
Angular CDK API. (n.d.). Retrieved from https://angular.io/api/cdk/portal/ComponentPortal