Angular Signals vs RxJS: which should your cursor rules enforce?

March 2026·8 min read

If you have been working with Angular for any length of time, you have almost certainly used RxJS. Observables, subjects, operators like switchMap and combineLatest -- these have been the backbone of reactive programming in Angular since the very beginning. Then Angular 16 arrived and introduced Signals, a fundamentally different approach to reactivity that is simpler, more predictable, and requires far less ceremony.

Now teams face a real question: which one should you standardize on? And more importantly, if you are using an AI code assistant like Cursor, which approach should your cursor rules enforce? Because without explicit guidance, your AI will happily mix both paradigms in the same component, and that is where things get messy.

What are Angular Signals?

Signals were introduced in Angular v16 as a new reactive primitive. At their core, a Signal is a wrapper around a value that notifies consumers when that value changes. You create one with thesignal() function, read it by calling it like a function, and update it with set() orupdate().

The magic happens with computed(), which creates a derived Signal that automatically recalculates when its dependencies change. There is also effect() for running side effects when Signals change. If you have used React's useState and useMemo, the mental model is surprisingly similar.

What makes Signals special in the Angular context is their relationship with change detection. Signals give Angular fine-grained knowledge of exactly which parts of your template depend on which pieces of state. This means the framework can skip re-checking components that have not changed, which is a significant performance improvement over the traditional zone.js approach.

How Signals differ from RxJS BehaviorSubject

On the surface, a Signal and a BehaviorSubject look similar. Both hold a current value. Both notify consumers when that value changes. But the differences go deeper than syntax.

A BehaviorSubject is an Observable. That means it participates in the entire RxJS ecosystem of operators, subscriptions, and teardown logic. You subscribe to it, you pipe it through operators, and you have to remember to unsubscribe or use patterns like takeUntilDestroyed to avoid memory leaks. It is powerful, but it comes with real overhead both in terms of bundle size and cognitive load.

A Signal, by contrast, is synchronous. You read it, you get the current value. There is no subscription to manage, no pipe chain to construct, no memory leak to worry about. The trade-off is that Signals do not handle asynchronous streams. They are not designed to model events over time the way Observables are. They model state -- a value that exists right now and might change later.

This distinction matters. RxJS excels at describing flows: an HTTP request that can be retried, a WebSocket stream that needs buffering, a debounced search input. Signals excel at describing snapshots: the current user, the selected tab, a derived total price. If you try to use Signals where you need streams, you end up writing imperative glue code that defeats the purpose. If you use RxJS where you just need local state, you add complexity for no benefit.

When Signals are the right choice

For most component-level state, Signals are the better option. If you are tracking whether a dropdown is open, storing form values, managing a loading flag, or computing a filtered list from a parent input, Signals are simpler, faster to write, and easier to read.

Derived state is where Signals really shine. With RxJS, creating a derived value means setting up a combineLatest or a switchMap, subscribing, and making sure the subscription is cleaned up. With computed(), you write a function that references other Signals, and the framework handles the rest. No subscription management. No operators. Just a function that returns a value.

Input Signals, introduced in Angular v17.1, take this further. You can now declare component inputs as Signals, which means your entire component state graph can be Signal-based from inputs all the way down to the template. This makes components easier to reason about because there is a single, consistent reactivity model.

If your cursor rules specify Signals as the default for component state, your AI assistant will generate cleaner, more modern Angular code. It will not reach for BehaviorSubject when a simple signal() would do. It will not create unnecessary subscriptions. And the code it generates will be more consistent with where Angular is heading as a framework.

When RxJS is still necessary

RxJS is not going away, and it should not. There are entire categories of problems where Observables are the right tool and Signals are not.

Complex async workflows are the clearest example. If you need to debounce a search input, switch to a new HTTP request when the query changes, and cancel the previous request, RxJS does this elegantly with a single pipe chain. Trying to replicate that behavior with Signals would require manual timers, manual cancellation, and a lot of imperative code.

Event streams are another case. WebSocket connections, server-sent events, real-time data feeds -- these are inherently asynchronous streams of values over time. Observables were designed for exactly this pattern. Signals were not.

The Angular Router and HttpClient still return Observables, and that is unlikely to change in the near future. While Angular provides interop functions like toSignal() andtoObservable() to bridge the two worlds, there are scenarios where staying in Observable-land is cleaner than converting back and forth.

Your cursor rules should account for this. A good rule set does not ban RxJS entirely. It says something like: "Use Signals for component state and derived values. Use RxJS for async streams, complex event handling, and when interacting with APIs that return Observables. Do not mix both in the same concern."

The learning curve trade-off

One of the strongest arguments for Signals is the learning curve. RxJS is notoriously difficult for developers who are new to reactive programming. The difference between switchMap, mergeMap, concatMap, and exhaustMap is subtle, and using the wrong one leads to bugs that are hard to diagnose. Subjects, multicasting, cold vs hot Observables -- these concepts take time to internalize.

Signals flatten that curve significantly. A senior developer can explain signal(), computed(), and effect() to a junior in fifteen minutes. The mental model is straightforward: you have values, some are derived from others, and when a source value changes, the derived values update automatically. There are no operators to memorize, no subscription lifecycle to manage.

This matters for AI-generated code too. When Cursor generates RxJS code, it sometimes picks the wrong operator or forgets to unsubscribe. The failure modes are subtle and hard to catch in review. When it generates Signal-based code, the potential mistakes are fewer and more obvious. A computed() that references the wrong Signal is easy to spot. A switchMap that should have been an exhaustMap is not.

If your team includes developers at different experience levels, standardizing on Signals for the common case and reserving RxJS for the cases that genuinely need it will reduce bugs and speed up onboarding.

Writing cursor rules for Signals and RxJS

The most effective cursor rules do not just say "use Signals." They explain the boundary between Signals and RxJS so the AI assistant can make the right call in ambiguous situations.

A practical rule set might include: use signal() for all local component state instead of plain class properties. Use computed() for any value that derives from other Signals. Use effect() only for side effects that cannot be handled in the template, and prefer template bindings where possible. For HTTP calls, use toSignal() with the HttpClient Observable. For complex async patterns like debounced search or polling, use RxJS with takeUntilDestroyed(). Never create a BehaviorSubject to hold state that could be a Signal.

Rules like these give Cursor enough context to make consistent decisions. Instead of guessing which paradigm to use, it follows the explicit guidance and generates code that fits your team's conventions. That is the whole point of cursor rules -- turning implicit knowledge into explicit instructions.

The pragmatic path forward

The Signals vs RxJS debate is not really a debate at all. It is a spectrum. Signals are the future of Angular's reactivity model, and for the vast majority of component-level concerns, they are the right choice. RxJS remains essential for async workflows and complex event handling.

The mistake teams make is not choosing one over the other. It is failing to choose at all and ending up with both paradigms used interchangeably throughout the codebase. That inconsistency is what makes code hard to maintain, hard to review, and hard for AI assistants to work with.

Set clear boundaries. Write them into your cursor rules. Let your AI assistant enforce the conventions so your team can focus on building features instead of debating patterns in every code review. The builder below generates rules that handle exactly this -- giving you a single source of truth for how reactivity should work in your project.

Try it yourself

Generate cursor rules that enforce the right balance of Signals and RxJS for your Angular project.

Open the builder