Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Signals: What's the proper way to trigger a fetch when input Signals change value?

So I've been learning and using Signals in Angular, and it's exciting. However, there are some use cases where I feel there's some friction. I can't figure out a good pattern when you have a component with input signals, and you want to trigger a re-fetch of data whenever some input value changes.

computed is obviously not the way to go since they can't be async. And effect, according to the docs, shouldn't modify component state. So that seems like a no-go as well. And ngOnChanges is being deprecated (long term) in favor of Signals-based components and zoneless.

Consider the following component:

@Component()
export class ChartComponent {
    dataSeriesId = input.required<string>();
    fromDate = input.required<Date>();
    toDate = input.required<Date>();

    private data = signal<ChartData | null>(null);
}

Whenever one of the input signals gets a new value, I want to trigger a re-fetch of data, and update the value of the private data signal.

How would one go about this? What's the best practice? Effect and bypass the rule to modify state?

like image 421
Magnus Wolffelt Avatar asked Sep 11 '25 04:09

Magnus Wolffelt


2 Answers

Angular 19 and later

Use Angular's rxResource, like this:

import { rxResource } from "@angular/core/rxjs-interop";

@Component({
  selector: "app-chart",
  template: `
    {{ resource.value() }}
    @if(resource.isLoading()) { Loading... } 
    @if(resource.error(); as error) { {{ error }} }
  `,
})
export class ChartComponent {
  dataSeriesId = input.required<string>();
  fromDate = input.required<Date>();
  toDate = input.required<Date>();

  params = computed(() => ({
    id: this.dataSeriesId(),
    from: this.fromDate(),
    to: this.toDate(),
  }));

  resource = rxResource({
    request: this.params,
    loader: (loaderParams) => this.fetchData(loaderParams.request),
  });

  private fetchData({ id, from, to }: { id: string; from: Date; to: Date }) {
    return of(`Example data for id [${id}] from [${from}] to [${to}]`).pipe(
      delay(1000)
    );
  }
}

Any change to the inputs will trigger a new fetch, cancelling the previous one. If fetchData uses Angular's HttpClient, the ongoing HTTP request will be automatically cancelled.

Working example on StackBlitz

Before Angular 19

Use Angular's rxjs-interop to convert the input signals to an Observable, then switchMap to fetch the results and then convert the result back to a signal, like this:

import { toObservable, toSignal } from "@angular/core/rxjs-interop";

@Component({
  selector: "app-chart",
  standalone: true,
  template: ` {{ data() }} `,
})
export class ChartComponent {
  dataSeriesId = input.required<string>();
  fromDate = input.required<Date>();
  toDate = input.required<Date>();

  params = computed(() => ({
    id: this.dataSeriesId(),
    from: this.fromDate(),
    to: this.toDate(),
  }));

  data = toSignal(
    toObservable(this.params).pipe(
      switchMap((params) => this.fetchData(params))
    )
  );

  private fetchData({ id, from, to }: { id: string; from: Date; to: Date }) {
    return of(`Example data for id [${id}] from [${from}] to [${to}]`).pipe(
      delay(1000)
    );
  }
}

Any change to any of the inputs will trigger a new fetch.

Working example on StackBlitz

like image 110
Rens Jaspers Avatar answered Sep 13 '25 20:09

Rens Jaspers


Using this utility function: https://ngxtension.netlify.app/utilities/signals/derived-async/ you can achieve it quite elegantly (fetching is a one-liner):

import { toObservable, toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-chart',
  standalone: true,
  template: `
    {{data()}}
  `,
})
export class ChartComponent {
  dataSeriesId = input.required<string>();
  fromDate = input.required<Date>();
  toDate = input.required<Date>();

  data = derivedAsync(() => this.fetchData$(dataSeriesId(), fromDate(), toDate());

  private fetchData$(id: string, from: Date, to: Date) {
    return of(`Example data for id [${id}] from [${from}] to [${to}]`).pipe(
      delay(1000)
    );
  }
}

(derived from Rens Jaspers' answer)

like image 32
ymajoros Avatar answered Sep 13 '25 20:09

ymajoros