Angular Signals bring a new way to manage state in Angular applications. They offer a simpler and more explicit model for synchronous state, especially at the component and service level. After migrating inputs, outputs, and local state, and then replacing BehaviorSubjects with Signals in services, a common question remains:
What should we do with RxJS?
Signals are powerful, but they are not a replacement for RxJS. Most existing Angular applications rely heavily on Observables for asynchronous logic, user interactions, and external events. Trying to fully replace RxJS with Signals often leads to over-engineered solutions or subtle bugs.
This post proposes a pragmatic migration strategy. Instead of choosing between Signals and RxJS, we will look at how to separate their responsibilities, and how to combine them effectively during a progressive migration. Through a few concrete examples, we will see when Signals make code simpler, when RxJS should remain the right tool, and how to bridge the two without rewriting large parts of an existing codebase.
1. Signals vs RxJS: stop choosing, start separating
When Angular Signals were introduced, many developers started to ask whether RxJS was still needed. In practice, this question is often the wrong one. Signals and RxJS solve different problems, and trying to replace one with the other usually makes the code harder to understand.
Signals are designed to manage synchronous state. They work well for UI state, derived values, and any data that can be read immediately by the template or by other parts of the application. Their main strength is simplicity: state is explicit, dependencies are clear, and updates are automatic through computed.
RxJS, on the other hand, is built for asynchronous streams and events. HTTP calls, user interactions, timers, WebSockets, and complex flows over time are still much easier to express with Observables and operators like switchMap, debounceTime, or retry.
A useful mental model is to stop choosing between Signals and RxJS, and start separating responsibilities. Use Signals to represent state, and use RxJS to handle events and asynchronous workflows. In most real-world applications, a hybrid approach leads to simpler and more maintainable code than trying to force everything into a single model.
Synchronous state
readonly isOpen = signal(false);
readonly canSubmit = computed(() => this.isOpen() && this.isValid());
Asynchronous state
search$ = this.searchInput$.pipe(
debounceTime(300),
switchMap(term => this.api.search(term))
);
2. Connecting signals and observables
Migrating to Signals rarely means removing RxJS. Most of the time, the goal is to connect existing Observable-based logic to a Signal-based state model. Angular provides two small but important APIs for this: toSignal() and toObservable().
The most common case is consuming an Observable and exposing its latest value as a Signal. This is useful when data comes from an HTTP call, a stream of events, or an existing service that already returns Observables.
readonly users = toSignal(
this.userService.users$,
{ initialValue: [] }
);
This allows the template and the component logic to work with a synchronous value, while RxJS continues to handle the asynchronous flow.
The opposite case also exists. Sometimes, an API or a library still expects an Observable, but your internal state is now managed with Signals. In this situation, toObservable() makes the integration straightforward.
readonly filters$ = toObservable(this.filters);
These two functions make migration incremental. They allow Signals and RxJS to coexist in the same codebase, without forcing a full rewrite or introducing complex abstractions. In practice, they are often the only tools needed to bridge the two worlds cleanly.
3. Common transition from observable component based to signal
A common migration step is converting an Observable used directly in a component into a Signal. This allows to simplify templates and reduce the reliance on the async pipe.
Before: Observable + async pipe
@Component({
selector: 'app-users',
imports: [AsyncPipe],
template: `
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
`,
})
export class UsersComponent {
private readonly usersService = inject(UsersService);
public readonly users$ = this.usersService.users$;
}
This pattern is very common and works well. However, it forces to import the AsyncPipe and makes it harder to reuse the data inside the component class.
After: Observable bridged to a Signal
@Component({
selector: 'app-users',
template: `
<ul>
<li *ngFor="let user of users()">
{{ user.name }}
</li>
</ul>
`,
})
export class UsersComponent {
private readonly usersService = inject(UsersService);
public readonly users = toSignal(this.usersService.users$, {
initialValue: [],
});
}
In this version, RxJS is still used to fetch and stream the data, but the component works with a synchronous Signal. The template becomes simpler, and the data can now be accessed directly inside the component logic without subscribing.
This approach is especially useful when migrating incrementally, as it avoids changing existing services while still benefiting from Signals at the component level.
4. Hybrid example: search input with filtering
Imagine a really common scenario where you have a user list filtered by a search input. The input events are asynchronous, so RxJS handles debouncing and HTTP requests. The filtered state is synchronous, so we can use a Signal for the filtered users.
@Component({
selector: 'app-users-search',
template: `
<input type="text" [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($event)" />
<ul>
<li *ngFor="let user of filteredUsers()">
{{ user.name }}
</li>
</ul>
`,
})
export class UsersSearchComponent {
// search term stored as a Signal
readonly searchTerm = signal('');
// convert Signal to Observable for async stream
private search$ = toObservable(this.searchTerm).pipe(
debounceTime(300),
switchMap(term => this.api.searchUsers(term))
);
// raw users fetched asynchronously
readonly users = toSignal(this.search$, { initialValue: [] });
// filtered users computed synchronously
readonly filteredUsers = computed(() =>
this.users().filter(user =>
user.name.toLowerCase().includes(this.searchTerm().toLowerCase())
)
);
}
Conclusion
Migrating to Signals does not mean giving up RxJS. In most real-world applications, the best results come from a hybrid approach:
- Signals handle synchronous state and computed values, making templates simpler and component logic more readable.
- RxJS continues to manage asynchronous streams, events, and complex flows over time.
- Using toSignal() and toObservable() allows a gradual migration, avoiding full rewrites and minimizing side effects.
Recent features like resource and rxResource look very promising for continuing this migration (documentation: https://angular.dev/guide/signals/resource)
They provide new tools to manage async data and http requests in a reactive way, and will make it even easier to integrate Signals and RxJS in the same codebase. A dedicated post will cover these APIs in detail.