DEV Community

Cover image for Angular Migrating from *ngFor to @for: A Complete Guide with Nested Examples
Prasun Chakraborty
Prasun Chakraborty

Posted on

Angular Migrating from *ngFor to @for: A Complete Guide with Nested Examples

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>
Enter fullscreen mode Exit fullscreen mode

New Syntax (@for)

@for (item of items; track item.id; let i = $index) {
  <div>{{ item.name }}</div>
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

Key changes:

  • *ngFor@for
  • let i = indexlet 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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode
trackByFn(index: number, item: any): any {
  return item.id;
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

3. Use OnPush Change Detection

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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 -->
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 or track $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:

  1. Always include tracking for optimal performance
  2. Use $index instead of index for index variables
  3. Wrap content in braces and close properly
  4. Choose tracking strategy based on your data structure
  5. 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


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)