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:
@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:
- Fetch new data when any input changes
- Avoid unsafe patterns like state modification in effects
- Work within Angular's signal-based future (since
ngOnChanges
is deprecated long-term) - Cancel ongoing requests when inputs change
Key Constraints
computed()
can't handle async operationseffect()
should not modify component state- Solutions must be compatible with Angular's zoneless future
Recommended Solutions
Angular 19+ (Official Approach)
Use Angular's rxResource
(or resource
) API for proper handling of asynchronous fetches triggered by signal changes.
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
, andstatus
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:
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
- Always handle loading states explicitly
- Use
switchMap
for automatic request cancellation - Wrap in
toSignal
for signal-based consumption - Consider adding error handling with
catchError
Third-Party Alternative (ngxtension)
For Angular versions before v19, use derivedAsync
from ngxtension:
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
Effect-Based State Mutation (Not Recommended)
DANGER
Modifying signals inside an effect compromises Angular's change detection safety
// 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:
- Manual subscription management required
- Error-prone cancellation logic
- Requires dangerous
allowSignalWrites
flag - Not compatible with zoneless change detection
Performance Considerations
- Combine inputs: Aggregate related inputs with
computed()
to avoid multiple simultaneous fetches:
// Good: Single combined trigger
params = computed(() => ({ ... }));
// Bad: Multiple independent triggers
id$ = toObservable(this.dataSeriesId);
date$ = toObservable(this.fromDate);
- Debounce inputs: Add debouncing for frequently changing values:
toSignal(
toObservable(this.params).pipe(
debounceTime(300), // Wait for input stability
distinctUntilChanged(), // Skip duplicate values
switchMap(/* fetch */)
)
);
Comparison of Approaches
Method | Angular Version | Cancel Previous | Loading State | Error Signal | Complexity |
---|---|---|---|---|---|
rxResource | 19+ | ✅ | ✅ | ✅ | Low |
toSignal /toObservable | >=17 | ✅ | Manual | Manual | Medium |
derivedAsync | >=16 | ❌ | Manual | Manual | Low |
effect -based | Any | Manual | Manual | Manual | High |
Modern Angular Best Practices
Use the resource pattern whenever possible:
- Reduces error-prone boilerplate
- Follows Angular team's recommendations
- Prepared for future zoneless changes
- Provides declarative template states
Advanced Use Cases
For multiple signal dependencies with complex refresh logic:
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.