Skip to content

Triggering Data Fetch on Input Signal Changes in Angular

Problem Statement

When working with Angular components using Signals, a common challenge arises: how to properly trigger data fetches when input signals change value. Consider a chart component with several input signals:

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

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

You need to:

  1. Fetch new data when any input changes
  2. Avoid unsafe patterns like state modification in effects
  3. Work within Angular's signal-based future (since ngOnChanges is deprecated long-term)
  4. Cancel ongoing requests when inputs change

Key Constraints

  • computed() can't handle async operations
  • effect() should not modify component state
  • Solutions must be compatible with Angular's zoneless future

Angular 19+ (Official Approach)

Use Angular's rxResource (or resource) API for proper handling of asynchronous fetches triggered by signal changes.

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

@Component({
  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: ({ request }) => this.fetchData(request)
  });

  private fetchData(params: { id: string; from: Date; to: Date }) {
    return this.http.get(`/data`, { params }); // HttpClient example
  }
}

Key Benefits

  • Automatic cancellation: Previous requests canceled on new inputs
  • Built-in state handling: isLoading, error, and status signals
  • Cleanup: Unsubscribes automatically on component destruction
  • Type safety: Strong typing throughout the flow

Pre-Angular 19 Approach

Combine signals with RxJS using Angular's interoperability utilities:

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

@Component({
  template: `{{ data() }} @if (loading()) { Loading... }`,
})
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(),
  }));

  loading = signal(false);
  
  data = toSignal(
    toObservable(this.params).pipe(
      switchMap(params => {
        this.loading.set(true);
        return this.fetchData(params).pipe(
          finalize(() => this.loading.set(false))
        );
      })
    )
  );

  private fetchData(params: { 
    id: string; 
    from: Date; 
    to: Date 
  }) {
    return this.http.get('/data', { params });
  }
}

Implementation Notes

  1. Always handle loading states explicitly
  2. Use switchMap for automatic request cancellation
  3. Wrap in toSignal for signal-based consumption
  4. Consider adding error handling with catchError

Third-Party Alternative (ngxtension)

For Angular versions before v19, use derivedAsync from ngxtension:

typescript
import { derivedAsync } from 'ngxtension/derived-async';

@DataComponent()
export class ChartComponent {
  // ...inputs same as before

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

  private fetchData(id: string, from: Date, to: Date) {
    return this.http.get(`/data/${id}`, { params: { from, to }});
  }
}

Anti-Patterns to Avoid

DANGER

Modifying signals inside an effect compromises Angular's change detection safety

typescript
// Discouraged approach - use only if absolutely necessary
let subscription: Subscription | null = null;

effect(() => {
  subscription?.unsubscribe();
  subscription = this.fetchData(
    this.dataSeriesId(), 
    this.fromDate(), 
    this.toDate()
  ).subscribe(data => {
    this.data.set(data); // Explicit allowSignalWrites required
  });
}, { allowSignalWrites: true }); // Bypasses safety mechanisms

Why avoid this pattern:

  1. Manual subscription management required
  2. Error-prone cancellation logic
  3. Requires dangerous allowSignalWrites flag
  4. Not compatible with zoneless change detection

Performance Considerations

  1. Combine inputs: Aggregate related inputs with computed() to avoid multiple simultaneous fetches:
typescript
// Good: Single combined trigger
params = computed(() => ({ ... }));

// Bad: Multiple independent triggers
id$ = toObservable(this.dataSeriesId);
date$ = toObservable(this.fromDate);
  1. Debounce inputs: Add debouncing for frequently changing values:
typescript
toSignal(
  toObservable(this.params).pipe(
    debounceTime(300),  // Wait for input stability
    distinctUntilChanged(), // Skip duplicate values
    switchMap(/* fetch */)
  )
);

Comparison of Approaches

MethodAngular VersionCancel PreviousLoading StateError SignalComplexity
rxResource19+Low
toSignal/toObservable>=17ManualManualMedium
derivedAsync>=16ManualManualLow
effect-basedAnyManualManualManualHigh

Modern Angular Best Practices

Use the resource pattern whenever possible:

  1. Reduces error-prone boilerplate
  2. Follows Angular team's recommendations
  3. Prepared for future zoneless changes
  4. Provides declarative template states

Advanced Use Cases

For multiple signal dependencies with complex refresh logic:

typescript
export class DashboardComponent {
  filters = signal({ ... });
  refreshTrigger = signal(0);
  
  resource = rxResource({
    request: computed(() => ({
      filters: this.filters(),
      version: this.refreshTrigger()
    })),
    loader: ({ request }) => this.loadDashboard(request)
  });

  // Force manual refresh
  refresh() {
    this.refreshTrigger.update(v => v + 1);
  }
}

Conclusion

For Angular 19+, leverage rxResource for signal-triggered fetches with built-in state management. For older Angular versions, use the toObservable + switchMap + toSignal pattern. Avoid effect-based solutions and imperative approaches—they compromise Angular's reactivity model and increase bug risks. The resource pattern provides the safest, most maintainable approach for data-triggered changes in modern Angular applications.