A green and black visual used as featured images for DevHub's article Angular Signals and Migrating from RxJS-Heavy Architecture.

Angular Signals and Migrating from RxJS-Heavy Architecture

If you’ve been working with Angular for a while, RxJS is most likely baked into how you think about state and data flow. It’s powerful, it handles complex async scenarios well, and it’s been the default answer to reactivity in Angular for years.

As projects grow in complexity, certain patterns start demanding more attention: subscription management, boilerplate across components and services, and a learning curve that rewards experience but takes time to climb.

Angular Signals were built to complement RxJS, covering the areas where a lighter approach to state management fits the job better.

Built on reactive programming

Reactive programming is the principle both Signals and RxJS are built on. The core idea behind it is that data changes should automatically propagate to the user interface without requiring manual refreshes or constant polling. When the state changes, the interface updates.

Think of a smart thermostat. You don’t keep asking whether it’s hot. It reacts automatically when the temperature changes. In code, reactive programming means building apps where data changes reach every part of the UI that cares about them, automatically.

What Angular Signals are

A Signal is a wrapper around a value that notifies any consumer when that value changes. You read it by calling it as a function, update it with .set() or .update(), and derive values from it with computed(). A spreadsheet cell works the same way: change one value and every formula depending on it updates instantly. Or a mailbox: when new mail arrives, you’re automatically notified, you don’t need to keep checking. A traffic light takes it further — when it changes, everyone watching reacts without being asked. Signals bring that same automatic reaction to Angular component state.

Signals in practice

The three core primitives cover most use cases:

  • signal(), which holds a value you read by calling it as a function and update with .set() or .update()
  • computed(), which derives a new value from one or more signals and stays in sync automatically
  • effect(), which runs a side effect whenever a signal it reads changes

What RxJS is

RxJS treats data as streams, much like a plumbing system where data flows through pipes. You can filter bad events, merge multiple sources, map values, and debounce — very powerful, but complex plumbing gets hard to reason about quickly.

The core RxJS concepts are:

  • an Observable, which is a stream of future values or events over time;
  • an Observer, which listens to the stream;
  • a Subscription, which is the active connection between the two;
  • Operators, which transform, filter, or combine streams;
  • a Subject, which acts as both Observable and Observer at the same time.
For HTTP requests, WebSocket connections, complex timing logic, or combining multiple data sources, RxJS is the right tool. The trade-off is the complexity: subscriptions need to be managed, operators need to be learned, and memory leaks from forgotten unsubscribe() calls are one of the most common sources of bugs in Angular codebases.

How Signals and RxJS compare

Signals and RxJS are often compared as if one replaces the other, but they were built for different jobs. Signals are synchronous and built around the current value of state. They read like plain variables, have a minimal learning curve, and require almost no boilerplate. In templates, a Signal is called directly with {{ count() }}, no async pipe needed.

RxJS was designed for data that changes over time. It handles continuous streams where you can split the flow, add pipes, combine multiple sources, and control timing with operators. HTTP requests, WebSocket connections, and complex async logic still belong there. That power comes with a real cost: a large library of operators to learn, subscriptions to manage, and memory leaks waiting to happen when teardown logic gets missed.

Each tool has a clearly defined lane. Signals own the synchronous, UI-state side of an Angular app, while RxJS owns the async, event-driven side.

Why migrate to Signals

For Angular developers working in RxJS-heavy codebases, most of these benefits will feel familiar as solutions to problems they’ve already run into.

Less boilerplate

Signals eliminate the subscribe/unsubscribe cycle entirely. No more managing subscriptions manually, no more memory leaks from cleanups that got forgotten along the way.

Better performance

Rather than triggering broader re-renders, the framework gets precise information about what changed and updates only the components that actually depend on that value.

Easier to read

The code reads like plain variables. A developer joining a project doesn’t need to understand an Observable chain to follow what a component is doing.

Interoperable

Signals and RxJS work together without friction. toSignal() and toObservable() bridge the two, so there’s no need to choose one over the other.

Type safe

Full TypeScript support means Signal<number> always holds a number and is always available, with no ambiguity about what the value is at any given point.

The future of Angular

The framework is investing heavily in Signals, and getting familiar with them now puts you ahead of where Angular is going rather than catching up to it later.

How to approach the migration strategy

Signals and RxJS coexist in the same app without any issues, which means the transition can happen gradually, at whatever pace makes sense for the codebase. Trying to migrate everything at once is the fastest way to introduce bugs. A step by step approach works better:

1.Start with new components

Any new component state gets written with Signals from the start, while existing RxJS code stays untouched. It’s the safest way to get comfortable with the API before touching anything already working in production.

2. Replace BehaviorSubject

Once comfortable with the basics, the most impactful change is replacing BehaviorSubject<T> in services with signal<T>(). It’s a direct swap that delivers the clearest reduction in boilerplate across the codebase.

3. Bridge HTTP calls with toSignal()

HTTP calls don’t need to change. Keep HttpClient returning Observables and wrap them with toSignal() wherever the value needs to be consumed in a component or template. The async layer stays intact while the rest of the component works with Signals.

4. Update templates

With Signals in place, the async pipe can be replaced with direct signal calls. {{ mySignal() }} replaces {{ myObservable$ | async }}, removing a layer of indirection and making templates easier to read.

5.Keep RxJS where it belongs

WebSockets, complex merges, and retry logic stay with RxJS. The goal of migration isn’t to eliminate it but to use each tool where it actually fits: Signals for state, RxJS for streams.

Bridging the gap between Signals and RxJS

Adopting Signals doesn’t mean leaving RxJS behind. The two convert into each other freely through toSignal() and toObservable(), which means both can live and work together inside the same component.
toSignal() is the more commonly reached-for of the two. It wraps an Observable as a Signal, which removes the need for the async pipe when consuming an HTTP response in a template. toObservable() works in the opposite direction, taking a Signal and feeding it into an RxJS pipeline when stream operators are needed.

Common pitfalls to avoid

Working with Signals is straightforward once the mental model clicks, but a few things tend to trip developers up early on:

  • Forgetting to call Signals — write count() not count in templates.
  • Using effect() too eagerly — prefer computed() for derived values, use effect() only for DOM side effects.
  • Using Signals for async data — for HTTP and WebSockets, still use RxJS with toSignal().
  • Mutating signal objects directly — never mutate arrays or objects inside a signal directly, use .set() or .update().
  • Over-migrating at once — incremental migration is safer. Mixing Signals and RxJS in the same app is fine.
  • Ignoring input() signals — component inputs can now be Signals too. Use input() for a more modern approach

Now that we’ve covered the most common pitfalls, let’s look at how Angular 17 introduced a set of changes that take Signals even further into the component API itself.

Signal Inputs and Outputs in Angular 17

Angular 17 brings Signals closer to the core component API. Inputs declared with input() behave as Signals and work directly inside computed(), which opens up derived values based on component inputs without any additional wiring. Outputs follow the same pattern, replacing @Output() and EventEmitter with a more consistent syntax.
The practical difference is that component inputs are now first-class citizens in the reactive model. A value coming in from a parent component can feed directly into a computed() without needing to be manually synced or watched through ngOnChanges.

Where to go from here

Signals fit into an existing Angular codebase without asking for a complete rewrite. They work alongside RxJS, cover the parts of state management where RxJS was always overkill, and make the code easier to follow for everyone on the team. The migration can start with a single service and grow from there, at whatever pace the project allows.

Insights by Stefan Claici, Senior Frontend Developer

Share

Other Articles

Subscribe to Our Newsletter

Subscribe
to Our Newsletter

Like what we’re doing? Don’t miss a thing. 
Subscribe to our newsletter to get the latest news and tech insights in your inbox!

Abonati newsletter