I’m using Angular 19 with the new signals API (and OnPush strategy). I have a component that tracks the current URL as a signal, then for each item in an array (e.g., a menu), I create a computed signal to check if it matches the URL. Here’s a simplified version:
@Component({
template: `
<li *ngFor="let item of items">
<a [routerLink]="item.routerLink"
[class.active]="isActivated(item)()">
{{ item.label }}
</a>
</li>
`,
})
export class MenuComponent {
url = toSignal(
this.router.events.pipe(
filter(e => e instanceof NavigationEnd),
map(e => e.urlAfterRedirects)
)
);
isActivated(item: MenuItem) {
return computed(() => this.url()?.startsWith(item.routerLink) ?? false);
}
}
Question: Every time the template is checked, it calls isActivated(item), which creates a new computed signal instance for each item. I suspect there’s a more efficient way to do this. What’s the recommended pattern or approach to avoid creating a fresh computed for each item every time, while still keeping the application reactive to URL changes?
Exactly, that’s overthinking it a bit. You can achieve the same functionality by directly referencing the signal without wrapping it in a computed.
Here’s the simplified version:
@Component({
template: `
<li *ngFor="let item of items">
<a [routerLink]="item.routerLink"
[class.active]="isActivated(item)">
{{ item.label }}
</a>
</li>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MenuComponent {
url = toSignal(
this.router.events.pipe(
filter(e => e instanceof NavigationEnd),
map(e => e.urlAfterRedirects)
)
);
isActivated(item: MenuItem): boolean {
return this.url()?.startsWith(item.routerLink) ?? false;
}
}
This avoids creating new computed instances for each item. Angular automatically re-evaluates the method when the url signal changes, keeping everything reactive and efficient.
Below is proof that the signal updates the DOM element directly and not through change detection (else ngOnChanges
console.log will fire) hence, my answer is valid with actual proof:
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import {
provideRouter,
RouterOutlet,
RouterLink,
Router,
NavigationEnd,
} from '@angular/router';
import { filter, map } from 'rxjs';
@Component({
selector: 'app-one',
template: `one`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class One {}
@Component({
selector: 'app-two',
template: `two`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Two {}
@Component({
selector: 'app-root',
template: `
{{url()}}
<ul>
@for(item of items;track $index) {
<li>
<a [routerLink]="item.routerLink"
[class.active]="isActivated(item)">
{{ item.label }}
</a>
</li>
}
</ul>
<input/>
<router-outlet></router-outlet>
`,
imports: [RouterOutlet, RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
items = [
{ routerLink: '/1', label: 'one' },
{ routerLink: '/2', label: 'two' },
];
router = inject(Router);
url = toSignal(
this.router.events.pipe(
filter((e) => e instanceof NavigationEnd),
map((e) => e.urlAfterRedirects)
)
);
isActivated(item: any): boolean {
console.log('func called');
return this.url()?.startsWith(item.routerLink) ?? false;
}
ngOnChanges() {
console.log('on changes');
}
}
bootstrapApplication(App, {
providers: [
provideRouter([
{ path: '1', component: One },
{ path: '2', component: Two },
]),
],
});