Structural Directives: *ngIf and *ngFor
Structural directives are directives that change the shape of the DOM by adding, removing, or repeating elements, rather than just changing an element's appearance or behavior in place. In the classic Angular template syntax, they are recognizable by the leading asterisk (*), a shorthand that the compiler desugars into an <ng-template> wrapping the host element. *ngIf conditionally instantiates or destroys a subtree based on a boolean expression, and *ngFor instantiates a template once per item in an iterable, giving each instantiation its own embedded view with local template variables. Understanding these two directives is foundational because most Angular applications built before v17, and many still today, rely on them extensively for conditional rendering and list rendering.
Cricket analogy: Structural directives reshaping the DOM are like a ground crew rolling the covers on and off the pitch (*ngIf) or laying out a fresh set of boundary ropes for each fielding position (*ngFor), physically changing the field rather than just repainting lines on it.
How *ngIf Works Under the Hood
When you write <div *ngIf="isVisible">Hello</div>, Angular desugars this into <ng-template [ngIf]="isVisible"><div>Hello</div></ng-template>. The NgIf directive holds a reference to the TemplateRef and a ViewContainerRef; when the bound expression is truthy it calls viewContainerRef.createEmbeddedView(templateRef), and when it becomes falsy it clears the container. Critically, this means the element and everything inside it — including child components — are fully destroyed and recreated, not merely hidden with CSS. That has real consequences: component state resets, ngOnInit fires again, and any subscriptions set up in a child component's constructor are torn down and rebuilt. *ngIf also supports an else clause, and an as clause to alias the evaluated expression, which is especially useful for narrowing an Observable's emitted value obtained via the async pipe.
Cricket analogy: *ngIf destroying and recreating the subtree is like a batter being given out and having to walk off and a completely new batter walking in from the pavilion, resetting their innings (state) from zero, rather than the same batter just resting between overs.
@Component({
selector: 'app-user-panel',
standalone: true,
imports: [NgIf, AsyncPipe],
template: `
<div *ngIf="user$ | async as user; else loading">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<ng-template #loading>
<p>Loading user…</p>
</ng-template>
`
})
export class UserPanelComponent {
@Input() user$!: Observable<User>;
}How *ngFor Works and Tracking Items
*ngFor="let item of items" repeats its host template once per element in the items iterable, exposing local template variables such as index, first, last, even, and odd inside the loop. Each iteration produces its own embedded view. By default, when the bound array reference changes, Angular's default differ compares items by identity to decide what to re-render, which can be expensive or produce jarring UI flicker for objects that are recreated on every fetch. The trackBy function fixes this: you supply a function that returns a stable identifier (typically an id) for each item, and Angular uses that to match old and new views, only creating or destroying DOM nodes for items that actually appeared or disappeared, and reusing (and thus preserving) DOM and component state for items that persisted.
Cricket analogy: trackBy is like a scorecard app matching batters by player ID across overs instead of by list position, so if the batting order gets refetched, Kohli's tile stays visually stable instead of the app tearing down and rebuilding every player card.
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [NgFor],
template: `
<ul>
<li *ngFor="let todo of todos; trackBy: trackById; let i = index">
{{ i + 1 }}. {{ todo.title }}
</li>
</ul>
`
})
export class TodoListComponent {
@Input() todos: Todo[] = [];
trackById(index: number, todo: Todo): number {
return todo.id;
}
}The asterisk is pure syntactic sugar. *ngIf="cond" and *ngFor="let x of xs" are both rewritten by the Angular compiler into an <ng-template> with a corresponding property or 'microsyntax' binding — you can write the desugared form by hand, and doing so is a good way to build intuition for how structural directives actually operate on TemplateRef and ViewContainerRef.
A single host element can only carry one structural directive. Writing <div *ngIf="a" *ngFor="let x of xs"> is a compile error. To combine conditional and repeated rendering, nest an <ng-container *ngIf="a"> around an element that carries *ngFor, or, in modern Angular, simply nest @if inside @for (or vice versa) since the new control-flow blocks don't have this one-directive-per-element restriction.
Because *ngIf and *ngFor come from CommonModule, standalone components must explicitly import NgIf and NgFor (or the whole CommonModule) to use them in templates — Angular's standalone API does not implicitly make these directives available, which is a common source of 'is not a known property' template compile errors for developers new to standalone components.
Cricket analogy: Needing to explicitly import NgIf/NgFor in standalone components is like a guest player needing formal registration papers filed before they're allowed onto the pitch for a specific match, even though everyone assumes any cricketer can just walk in and play.
- *ngIf and *ngFor are structural directives, marked with a leading asterisk, that reshape the DOM rather than just styling elements.
- The asterisk syntax desugars to an <ng-template> bound to the directive, using TemplateRef and ViewContainerRef under the hood.
- *ngIf fully destroys and recreates its content on toggle, resetting component and subscription state — it does not just hide elements.
- *ngIf supports else and as clauses, commonly paired with the async pipe to unwrap Observables in the template.
- *ngFor's default change tracking can cause unnecessary DOM churn; a trackBy function keyed on a stable id avoids this.
- Only one structural directive may be applied per host element; use ng-container or nesting to combine conditions and loops.
Practice what you learned
1. What does *ngIf="isVisible" desugar into?
2. Why does using trackBy with *ngFor matter for performance?
3. What happens to a child component's state when its parent *ngIf condition flips from true to false and back to true?
4. In a standalone component, what must you do to use *ngFor in the template?
5. Why is <div *ngIf="a" *ngFor="let x of xs"> invalid?
Was this page helpful?
You May Also Like
The New Control Flow Syntax (@if, @for, @switch)
Explore Angular's built-in @if, @for, and @switch template control-flow blocks, introduced to replace *ngIf/*ngFor/*ngSwitch with faster, more ergonomic syntax.
Template Syntax and Interpolation
How Angular templates blend HTML with expressions, and how interpolation renders component state directly into the DOM as text.
Change Detection and Zone.js
Understand how Angular detects and applies UI updates via Zone.js, how the change detection tree works, and how zoneless change detection with signals changes the model.
Common Angular Pitfalls
A practical rundown of mistakes Angular developers repeatedly make—memory leaks, signal misuse, change detection surprises, and DI scoping errors—and how to avoid each.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics