Hone logo
Hone
Problems

Mastering Angular Change Detection

Angular's change detection mechanism is fundamental to how your application updates the view when data changes. Understanding and effectively utilizing it is crucial for building performant and responsive Angular applications. This challenge will test your ability to implement custom change detection strategies and leverage Angular's built-in mechanisms.

Problem Description

Your task is to implement a custom change detection strategy within an Angular component. You will be given a component that displays a list of items, and you need to ensure that the component's view updates correctly only when a specific property of an item changes. This is to optimize performance by avoiding unnecessary re-renders.

Key Requirements:

  1. Create an Angular component: This component will manage a list of Product objects. Each Product object will have at least an id (number), a name (string), and a price (number).
  2. Implement a custom change detection strategy: Use ChangeDetectionStrategy.OnPush for your component. This strategy dictates that the component will only be checked if:
    • An @Input() property changes (referential check).
    • An event originates from the component itself.
    • The async pipe is used.
    • Change detection is explicitly marked (e.g., via ChangeDetectorRef.markForCheck()).
  3. Simulate data updates: Implement methods within the component to update the price of a specific product and to add a new product to the list.
  4. Display the product list: The template should iterate through the product list and display the name and price of each product.
  5. Demonstrate OnPush behavior: The solution should clearly show that updating a product's price without a new reference to the product object itself does not trigger a view update by default, and how to correctly trigger it if needed.

Expected Behavior:

  • When a product's price is updated directly (mutating the object), the view should not update unless markForCheck() is called.
  • When a new product is added to the list (creating a new array reference), the view should update.
  • When the OnPush strategy is applied, the component should only re-render when necessary, leading to better performance.

Edge Cases:

  • Handling an empty product list.
  • Ensuring that adding a new product correctly updates the list display.
  • Demonstrating how to trigger an update when a nested property of an input changes with OnPush.

Examples

Example 1:

Component Logic (Conceptual):

// product.model.ts
export interface Product {
  id: number;
  name: string;
  price: number;
}

// product-list.component.ts
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
import { Product } from './product.model';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush // Crucial part
})
export class ProductListComponent {
  @Input() products: Product[] = [];

  constructor(private cdr: ChangeDetectorRef) {}

  updateProductPrice(productId: number, newPrice: number): void {
    const product = this.products.find(p => p.id === productId);
    if (product) {
      // This is a direct mutation. Without markForCheck(), OnPush won't re-render.
      product.price = newPrice;
      // To force an update:
      // this.cdr.markForCheck();
    }
  }

  addProduct(newProduct: Product): void {
    // Creating a new array reference. This will trigger re-render with OnPush.
    this.products = [...this.products, newProduct];
  }
}

Template (product-list.component.html):

<h2>Product List</h2>
<div *ngIf="products.length === 0">No products available.</div>
<ul>
  <li *ngFor="let product of products">
    {{ product.name }} - ${{ product.price.toFixed(2) }}
  </li>
</ul>
<button (click)="updateProductPrice(1, 15.99)">Update Product 1 Price (No MarkForCheck)</button>
<button (click)="updateProductPrice(1, 16.99); cdr.markForCheck()">Update Product 1 Price (With MarkForCheck)</button>
<button (click)="addProduct({ id: 4, name: 'New Gadget', price: 99.99 })">Add New Product</button>

Scenario: Initial products input: [{ id: 1, name: 'Laptop', price: 1200.00 }, { id: 2, name: 'Mouse', price: 25.00 }]

Action: User clicks "Update Product 1 Price (No MarkForCheck)".

Expected Output: The price for 'Laptop' in the list will not visually change.

Explanation: The updateProductPrice method mutated the price property of an existing Product object. Since the component uses ChangeDetectionStrategy.OnPush and markForCheck() was not called, Angular's change detection did not detect a change in the input reference or an event originating from the component, and thus did not re-render the view.

Example 2:

Scenario: Continuing from Example 1, user clicks "Add New Product".

Expected Output: The list now displays:

  • Laptop - $1200.00
  • Mouse - $25.00
  • New Gadget - $99.99

Explanation: The addProduct method created a new array by spreading the existing products and adding the newProduct. This change in the array reference for the @Input() products property is detected by Angular's OnPush strategy, triggering a re-render and displaying the new product.

Example 3:

Scenario: Continuing from Example 1, user clicks "Update Product 1 Price (With MarkForCheck)".

Expected Output: The price for 'Laptop' in the list will visually change to $16.99.

Explanation: Although the price property was mutated directly, the explicit call to this.cdr.markForCheck() informed Angular that the component (and its children that depend on its inputs) needs to be checked for changes. This overrides the default OnPush behavior for this specific update, causing the view to re-render and reflect the new price.

Constraints

  • Your solution must be written in TypeScript within an Angular project.
  • The component must utilize ChangeDetectionStrategy.OnPush.
  • You should demonstrate updating both an existing product's property (without changing object reference) and adding a new product (changing array reference).
  • The Product interface should have id, name, and price properties as described.

Notes

  • The core of this challenge is understanding why OnPush works the way it does and how to interact with it.
  • Consider how you would handle deeply nested objects or arrays within your Product interface and how OnPush would behave.
  • Think about the implications of using the async pipe with OnPush.
  • You will likely need to create a parent component to provide the products input to your ProductListComponent.
Loading editor...
typescript