Understanding the Problem: Why Pure Pipes Don't React to Internal State Changes

In Angular, a pure pipe (which is the default with pure: true) is only executed when Angular detects a change in the input arguments passed to its transform method. Angular uses a strict reference check (===) to determine if the inputs have changed.

If your pipe's inputs (e.g., a number and a format string) remain the same, Angular will completely bypass the pipe's transform method to save performance. This happens even if you manually trigger change detection via ChangeDetectorRef.markForCheck() or subscribe to a language change event inside the pipe's constructor. Angular's compiler simply does not watch internal pipe state for pure pipes.

While setting pure: false forces the pipe to run on every single change detection cycle, this is highly inefficient and can cause severe performance degradation in large applications.

The Best Solutions to React to Language/State Changes

Here are the three most effective and modern ways to solve this issue while keeping your application performant.

Solution 1: Pass the Dynamic State (Language) as a Pipe Parameter (Recommended)

The cleanest, most idiomatic way to handle this in Angular is to pass the current language directly into the pipe as an argument. Whenever the language changes, the pipe's inputs change, prompting Angular to automatically recalculate the pure pipe.

First, update your pipe to accept the language parameter:

@Pipe({
    name: 'myNumber',
    pure: true,
    standalone: true
})
export class VNumberPipe implements PipeTransform {
    private intl = inject(IntlService);

    transform(value: number, format: string, lang: string): string {
        return this.intl.formatNumber(value, format, lang);
    }
}

Next, in your component, expose the language as an Observable or a Signal:

@Component({
    // ...
})
export class MyComponent {
    translate = inject(TranslateService);
    // Expose language as an observable
    currentLang$ = this.translate.onLangChange.pipe(
        map(event => event.lang),
        startWith(this.translate.currentLang)
    );
}

Finally, pass the language to the pipe in your HTML template using the async pipe:

<p>{{ myNumberValue | myNumber: 'decimal' : (currentLang$ | async) }}</p>

Solution 2: Leverage Angular Signals (Angular 16+)

If you are using Angular 16 or newer, you can replace the Observable with a Signal. This makes your template code cleaner and avoids the need for the async pipe.

In your component:

import { signal, inject, effect } from '@angular/core';

export class MyComponent {
    translate = inject(TranslateService);
    currentLang = signal(this.translate.currentLang);

    constructor() {
        this.translate.onLangChange.subscribe(event => {
            this.currentLang.set(event.lang);
        });
    }
}

In your template, simply execute the signal to pass its current value:

<p>{{ myNumberValue | myNumber: 'decimal' : currentLang() }}</p>

Solution 3: Create a Memoized Impure Pipe

If you absolutely cannot pass the language as a parameter to the template, you can use an impure pipe (pure: false) but implement a custom memoization (caching) mechanism. This ensures that the heavy formatting logic only runs when either the value, format, or language actually changes.

@Pipe({
    name: 'myNumber',
    pure: false,
    standalone: true
})
export class VNumberPipe implements PipeTransform, OnDestroy {
    private intl = inject(IntlService);
    private translate = inject(TranslateService);
    private destroyRef = inject(DestroyRef);

    private lastValue!: number;
    private lastFormat!: string;
    private lastLang!: string;
    private cachedResult!: string;

    constructor() {
        this.translate.onLangChange.pipe(
            takeUntilDestroyed(this.destroyRef)
        ).subscribe(l => this.lastLang = l.lang);
        
        this.lastLang = this.translate.currentLang;
    }

    transform(value: number, format: string): string {
        // Only recalculate if one of the dependencies changed
        if (
            value !== this.lastValue || 
            format !== this.lastFormat || 
            this.translate.currentLang !== this.lastLang
        ) {
            this.lastValue = value;
            this.lastFormat = format;
            this.lastLang = this.translate.currentLang;
            this.cachedResult = this.intl.formatNumber(value, format, this.lastLang);
        }

        return this.cachedResult;
    }
}

Summary

  • Pure pipes do not monitor internal service state or subscriptions. They only re-run when their input parameters change.
  • The best architectural approach is Solution 1 or 2: pass the active language directly into the pipe parameters from the template.
  • If you must use pure: false, always implement caching/memoization (Solution 3) to prevent performance degradation during Angular's digest cycles.