Introduction
Angular 17 introduced a new control flow syntax that replaces the traditional structural directives like *ngFor
, *ngIf
, and *ngSwitch
. This new syntax is more performant, provides better type safety, and follows modern JavaScript patterns. In this article, we'll explore how to migrate from *ngFor
to the new @for
syntax, using a real-world nested example from a profile builder component.
Why Migrate to @for?
Performance Benefits
-
Built-in tracking: The new
@for
syntax includes automatic tracking optimization - Reduced bundle size: Eliminates the need for structural directive imports
- Better tree-shaking: More efficient dead code elimination
Developer Experience
- Type safety: Better TypeScript integration
- Cleaner syntax: More readable and intuitive
- Modern patterns: Aligns with current JavaScript/TypeScript standards
Future-Proofing
- Angular's direction: This is the future of Angular control flow
-
Deprecation path:
*ngFor
will eventually be deprecated
Basic Syntax Comparison
Old Syntax (*ngFor)
<div *ngFor="let item of items; let i = index; trackBy: trackByFn">
{{ item.name }}
</div>
New Syntax (@for)
@for (item of items; track item.id; let i = $index) {
<div>{{ item.name }}</div>
}
Key Differences Explained
1. Structural Directive vs Control Flow
-
Old: Uses structural directive syntax with asterisk (
*
) -
New: Uses control flow syntax with at symbol (
@
)
2. Tracking Mechanism
-
Old: Requires explicit
trackBy
function -
New: Built-in
track
keyword with automatic optimization
3. Index Variable
-
Old:
let i = index
-
New:
let i = $index
(note the dollar sign)
4. Content Wrapping
- Old: Content follows the directive
-
New: Content wrapped in curly braces
{}
Real-World Example: Profile Builder Component
Let's examine a complex nested structure from a profile builder component that manages dynamic sections and their items.
Original Code with *ngFor
<!-- Dynamic Sections -->
<ng-container formArrayName="sections">
<div *ngFor="let sec of sections.controls; let i = index" [formGroupName]="i">
<p-card class="bg-white dark:bg-slate-900 mb-4">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<button pButton type="button" icon="pi pi-sort" class="p-button-text p-button-sm"></button>
<label class="font-semibold">{{ sec.get('title')!.value }}</label>
<button pButton type="button" icon="pi pi-pencil" class="p-button-text p-button-sm" (click)="renameSection(i)"></button>
</div>
<div class="flex gap-2">
<button pButton type="button" label="Add" class="p-button-sm" (click)="addItem(i)"></button>
<button pButton type="button" label="Generate AI" class="p-button-sm" (click)="onGenerateAI(sec.get('title')!.value)"></button>
<button pButton type="button" icon="pi pi-trash" class="p-button-sm p-button-text text-red-500" (click)="removeSection(i)"></button>
</div>
</div>
<div formArrayName="items" class="space-y-2">
<div *ngFor="let itemCtrl of getItems(i).controls; let j = index" [formGroupName]="j" class="flex items-center gap-2">
<input formControlName="value" class="flex-1 border rounded p-2" placeholder="Item {{ j + 1 }}" />
<button pButton type="button" icon="pi pi-times" class="p-button-sm p-button-text text-red-500" (click)="removeItem(i, j)"></button>
</div>
</div>
</p-card>
</div>
</ng-container>
Migrated Code with @for
<!-- Dynamic Sections -->
<ng-container formArrayName="sections">
@for (sec of sections.controls; track i; let i = $index) {
<div [formGroupName]="i">
<p-card class="bg-white dark:bg-slate-900 mb-4">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<button pButton type="button" icon="pi pi-sort" class="p-button-text p-button-sm"></button>
<label class="font-semibold">{{ sec.get('title')!.value }}</label>
<button pButton type="button" icon="pi pi-pencil" class="p-button-text p-button-sm" (click)="renameSection(i)"></button>
</div>
<div class="flex gap-2">
<button pButton type="button" label="Add" class="p-button-sm" (click)="addItem(i)"></button>
<button pButton type="button" label="Generate AI" class="p-button-sm" (click)="onGenerateAI(sec.get('title')!.value)"></button>
<button pButton type="button" icon="pi pi-trash" class="p-button-sm p-button-text text-red-500" (click)="removeSection(i)"></button>
</div>
</div>
<div formArrayName="items" class="space-y-2">
@for (itemCtrl of getItems(i).controls; track j; let j = $index) {
<div [formGroupName]="j" class="flex items-center gap-2">
<input formControlName="value" class="flex-1 border rounded p-2" placeholder="Item {{ j + 1 }}" />
<button pButton type="button" icon="pi pi-times" class="p-button-sm p-button-text text-red-500" (click)="removeItem(i, j)"></button>
</div>
}
</div>
</p-card>
</div>
}
</ng-container>
Step-by-Step Migration Process
Step 1: Identify the Loop Structure
First, understand your nested structure:
-
Outer loop: Iterates over sections (
sections.controls
) -
Inner loop: Iterates over items within each section (
getItems(i).controls
)
Step 2: Convert Outer Loop
<!-- Before -->
<div *ngFor="let sec of sections.controls; let i = index" [formGroupName]="i">
<!-- After -->
@for (sec of sections.controls; track i; let i = $index) {
<div [formGroupName]="i">
Key changes:
-
*ngFor
→@for
-
let i = index
→let i = $index
- Added
track i
for performance - Wrapped content in
{ }
Step 3: Convert Inner Loop
<!-- Before -->
<div *ngFor="let itemCtrl of getItems(i).controls; let j = index" [formGroupName]="j">
<!-- After -->
@for (itemCtrl of getItems(i).controls; track j; let j = $index) {
<div [formGroupName]="j">
Key changes:
- Same pattern as outer loop
- Note that
i
is available from the outer loop context
Step 4: Close the Loops Properly
<!-- Inner loop closing -->
}
</div>
</p-card>
</div>
}
</ng-container>
Important: Each @for
block must be closed with }
Understanding the Track Keyword
What is Tracking?
Tracking helps Angular identify which items have changed, been added, or removed, enabling efficient DOM updates.
Track Options
1. Track by Index (Our Example)
@for (sec of sections.controls; track i; let i = $index)
- Uses the index as the tracking key
- Good for static lists where order doesn't change
- Use case: When items are rarely reordered
2. Track by Object Property
@for (item of items; track item.id)
- Uses a unique property as the tracking key
- Better for dynamic lists with frequent changes
- Use case: When items can be reordered or filtered
3. Track by Function
@for (item of items; track trackByFn)
trackByFn(index: number, item: any): any {
return item.id;
}
- Custom tracking function
- Most flexible approach
- Use case: Complex tracking logic
Why We Used track i
in Our Example
In our profile builder, sections are typically added/removed but rarely reordered, making index-based tracking sufficient and performant.
Common Migration Patterns
Pattern 1: Simple List
<!-- Before -->
<ul>
<li *ngFor="let item of items">{{ item.name }}</li>
</ul>
<!-- After -->
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
Pattern 2: With Index
<!-- Before -->
<div *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item.name }}
</div>
<!-- After -->
@for (item of items; track item.id; let i = $index) {
<div>{{ i + 1 }}. {{ item.name }}</div>
}
Pattern 3: With Multiple Variables
<!-- Before -->
<div *ngFor="let item of items; let i = index; let first = first; let last = last">
{{ item.name }}
</div>
<!-- After -->
@for (item of items; track item.id; let i = $index; let first = $first; let last = $last) {
<div>{{ item.name }}</div>
}
Pattern 4: Nested Loops (Our Case)
<!-- Before -->
<div *ngFor="let section of sections; let i = index">
<h3>{{ section.title }}</h3>
<div *ngFor="let item of section.items; let j = index">
{{ item.name }}
</div>
</div>
<!-- After -->
@for (section of sections; track i; let i = $index) {
<div>
<h3>{{ section.title }}</h3>
@for (item of section.items; track j; let j = $index) {
<div>{{ item.name }}</div>
}
</div>
}
Performance Considerations
1. Choose the Right Tracking Strategy
<!-- Good for static lists -->
@for (item of items; track $index)
<!-- Good for dynamic lists -->
@for (item of items; track item.id)
<!-- Good for complex scenarios -->
@for (item of items; track trackByFn)
2. Avoid Expensive Operations in Loops
<!-- Bad: Expensive operation in template -->
@for (item of items; track item.id) {
<div>{{ expensiveCalculation(item) }}</div>
}
<!-- Good: Pre-calculate in component -->
@for (item of processedItems; track item.id) {
<div>{{ item.calculatedValue }}</div>
}
3. Use OnPush Change Detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
Common Pitfalls and Solutions
Pitfall 1: Forgetting to Close Blocks
<!-- Wrong -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
<!-- Missing closing brace -->
<!-- Correct -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
Pitfall 2: Incorrect Index Variable
<!-- Wrong -->
@for (item of items; track item.id; let i = index)
<!-- Correct -->
@for (item of items; track item.id; let i = $index)
Pitfall 3: Missing Track Keyword
<!-- Wrong: No tracking -->
@for (item of items) {
<div>{{ item.name }}</div>
}
<!-- Correct: With tracking -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
Pitfall 4: Nested Loop Variable Conflicts
<!-- Wrong: Same variable name -->
@for (item of items; track item.id; let i = $index) {
@for (subItem of item.subItems; track subItem.id; let i = $index) {
<!-- 'i' conflicts! -->
}
}
<!-- Correct: Different variable names -->
@for (item of items; track item.id; let i = $index) {
@for (subItem of item.subItems; track subItem.id; let j = $index) {
<!-- 'i' and 'j' are distinct -->
}
}
Testing Your Migration
1. Visual Testing
- Ensure all items render correctly
- Check that dynamic content updates properly
- Verify that add/remove operations work
2. Performance Testing
// Before migration
console.time('ngFor-render');
// Render with *ngFor
console.timeEnd('ngFor-render');
// After migration
console.time('for-render');
// Render with @for
console.timeEnd('for-render');
3. Unit Testing
it('should render all sections', () => {
const sections = fixture.debugElement.queryAll(By.css('.section'));
expect(sections.length).toBe(component.sections.length);
});
it('should render all items in each section', () => {
const items = fixture.debugElement.queryAll(By.css('.item'));
const expectedItemCount = component.sections
.reduce((total, section) => total + section.items.length, 0);
expect(items.length).toBe(expectedItemCount);
});
Migration Checklist
- [ ] Identify all
*ngFor
instances in your templates - [ ] Choose appropriate tracking strategy for each loop
- [ ] Convert syntax:
*ngFor
→@for
- [ ] Update index variables:
index
→$index
- [ ] Add tracking:
track item.id
ortrack $index
- [ ] Wrap content in curly braces
{ }
- [ ] Close all loops with
}
- [ ] Test rendering and functionality
- [ ] Verify performance improvements
- [ ] Update unit tests if necessary
Conclusion
Migrating from *ngFor
to @for
is a significant step toward modern Angular development. The new syntax provides better performance, type safety, and developer experience. Our nested profile builder example demonstrates how to handle complex scenarios with multiple loops and form controls.
Key takeaways:
- Always include tracking for optimal performance
-
Use
$index
instead ofindex
for index variables - Wrap content in braces and close properly
- Choose tracking strategy based on your data structure
- Test thoroughly after migration
The migration process is straightforward but requires attention to detail, especially with nested structures. The performance benefits and future-proofing make this migration worthwhile for any Angular application.
Resources
- Angular Control Flow Documentation
- Angular Migration Guide
- Performance Best Practices
- Angular DevTools
This guide covers the migration from Angular's structural directive syntax to the new control flow syntax, using real-world examples from a profile builder component. The new syntax provides better performance and developer experience while maintaining the same functionality.
Top comments (0)