0 day streak
javascript / expert
Snippet

Structural Equality Guards for OnPush Component Trees

OnPush components re-render whenever an `@Input()` receives a new object reference, even if that object is structurally identical to the previous one — a common source of wasted change-detection cycles in Angular trees where parents rebuild filter or config objects on every render. Wrapping the setter in a structural equality check breaks that reference-identity coupling: the input setter runs on every reference change, but expensive recomputation only happens when the actual field values differ. This is distinct from `distinctUntilChanged` on an Observable stream because it guards a plain `@Input()` binding, which has no operator pipeline to intercept.

snippet.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
interface Filters {
status: readonly string[];
minPrice: number;
}
function sameFilters(a: Filters, b: Filters): boolean {
return (
a.minPrice === b.minPrice &&
a.status.length === b.status.length &&
a.status.every((s, i) => s === b.status[i])
);
}
@Component({
selector: 'app-catalog',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<app-list [items]="visibleItems" />`,
})
export class CatalogComponent {
private lastFilters: Filters | null = null;
visibleItems: unknown[] = [];
@Input() set filters(next: Filters) {
if (this.lastFilters && sameFilters(this.lastFilters, next)) {
return;
}
this.lastFilters = next;
this.visibleItems = this.recompute(next);
}
private recompute(f: Filters): unknown[] {
return [];
}
}
angular
Breakdown
1
if (this.lastFilters && sameFilters(this.lastFilters, next)) { return; }
Early-returns before reassigning `visibleItems`, so no new array reference is produced and the child `app-list` skips its own OnPush check.
2
a.status.length === b.status.length && a.status.every((s, i) => s === b.status[i])
Compares array contents by index rather than reference, since `readonly string[]` arrays rebuilt upstream will never be `===` even with identical values.
3
@Input() set filters(next: Filters) { ... }
Angular always invokes an input setter on reference change; the guard logic must live inside it since Angular provides no built-in equality check for inputs.