*ngIf and *ngFor are structural directives — they add or remove whole chunks of DOM based on a condition or a collection, rather than just toggling a class or style on an element that's already there. Both are used constantly, and both have a specific gotcha worth knowing before it causes a confusing template error.
1. *ngIf — conditional rendering
<div *ngIf="user">
Welcome, {{ user.name }}
</div>
Unlike CSS display: none, *ngIf="false" actually removes the element from the DOM entirely, not just visually hides it — this matters because it also destroys any component or directive instance inside it, running their ngOnDestroy lifecycle hooks, and re-creates them fresh if the condition later becomes true again.
2. *ngIf with an else block
<div *ngIf="user; else loggedOut">
Welcome, {{ user.name }}
</div>
<ng-template #loggedOut>
<a routerLink="/login">Log in</a>
</ng-template>
3. *ngFor — rendering a list
<li *ngFor="let product of products">
{{ product.name }}
</li>
4. trackBy — the performance detail most tutorials skip
<li *ngFor="let product of products; trackBy: trackByProductId">
{{ product.name }}
</li>
trackByProductId(index: number, product: Product): number {
return product.id;
}
Without trackBy, Angular tracks list items by object identity by default — if the entire products array is replaced with a new array of otherwise-identical-looking objects (a common result of a fresh API response), Angular can't tell the new objects apart from the old ones and destroys and re-creates every single list item's DOM, even for rows whose actual data didn't change. trackBy, returning a stable ID, lets Angular correctly match old and new items and only update what actually changed.
5. The rule that trips people up: can't combine *ngIf and *ngFor on the same element
// Not allowed as of Angular 17+ — throws a compile-time error
<li *ngFor="let product of products" *ngIf="product.inStock">
{{ product.name }}
</li>
Angular removed support for placing two structural directives on the same element specifically because the combined behavior was ambiguous and error-prone. The fix is wrapping one of them around an <ng-container>, which renders no actual DOM element of its own:
<ng-container *ngFor="let product of products">
<li *ngIf="product.inStock">{{ product.name }}</li>
</ng-container>
<ng-container> exists specifically for this situation — it's a template-only grouping element that never renders itself into the actual DOM, so it can carry one structural directive while its child carries the other, without introducing an extra wrapper element that would otherwise show up in the rendered output.
6. Modern alternative: the built-in @if and @for control flow
@for (product of products; track product.id) {
@if (product.inStock) {
<li>{{ product.name }}</li>
}
}
Angular 17 introduced this new built-in control-flow syntax as the current recommended approach going forward — it's generally faster (compiled more efficiently than the directive-based syntax) and requires an explicit track expression by design, which avoids the easy-to-forget trackBy performance pitfall from directive-based *ngFor by making it a required part of the syntax itself rather than an easily-skipped optional addition.