Using Signals in Angular 17: Goodbye NgRx for Simple State?
With the release of Angular 17, developers now have access to Signals—a new reactive primitive that promises a cleaner, more intuitive way to manage local component state. For years, we’ve leaned heavily on RxJS and NgRx to handle reactivity and global state management. But with Signals in the picture, the question arises:
Do we still need NgRx or RxJS for simple component state management?
In this post, we’ll explore that question through real-world examples and side-by-side comparisons of RxJS vs. Signals. By the end, you’ll have a clear picture of when to use Signals and when to stick with RxJS or NgRx.
RxJS vs. Signals: A Quick Comparison
| Feature | RxJS (BehaviorSubject, Observable) | Angular Signals (signal(), computed()) |
|---|---|---|
| API Complexity | Higher | Lower |
| Boilerplate | More setup | Minimal |
| Change Detection | Manual or via | async pipe | Automatic, fine-grained |
| Best For | Complex state, side effects | Local/component state |
✅ Example 1: Counter Component
With RxJS
counter$ = new BehaviorSubject<number>(0);
increment() {
this.counter$.next(this.counter$.value + 1);
}
<p>{{ counter$ | async }}</p>
<button (click)="increment()">Increment</button>
With Signals (Angular 17+)
import { signal } from '@angular/core';
counter = signal(0);
increment() {
this.counter.update(v => v + 1);
}
<p>{{ counter() }}</p>
<button (click)="increment()">Increment</button>
Why Signals Win Here: No | async pipe, no BehaviorSubject, and fewer moving parts.
Example 2: Toggle Visibility
RxJS Version
isVisible$ = new BehaviorSubject<boolean>(false);
toggle() {
this.isVisible$.next(!this.isVisible$.value);
}
Signals Version
isVisible = signal(false);
toggle() {
this.isVisible.update(v => !v);
}
Why Choose Signals: It reads more like regular JavaScript while still being reactive.
Example 3: Derived State with computed()
Let’s say you want to compute isLoggedIn from a user object.
RxJS Way
user$ = new BehaviorSubject<User | null>(null); isLoggedIn$ = this.user$.pipe(map(user => !!user));
Signals Way
user = signal<User | null>(null); isLoggedIn = computed(() => !!user());
Why Signals Shine: computed() is reactive and updates automatically when user() changes. No need for manual piping or subscribing. Learn more in the Angular docs.
🤔 So… Do We Still Need RxJS or NgRx?
✅ Use Signals when:
- You need local component state
- You’re working with UI flags, toggles, or counters
- You want to derive state from other signals (e.g.,
computed())
✅ Use RxJS/NgRx when:
- You’re dealing with asynchronous data (e.g., HTTP streams, WebSocket)
- You need global shared state across many components
- You’re implementing side effects or effects chains
- You want advanced features like time-travel debugging, devtools, or undo/redo
For more, check out Angular’s guide on state management.
Final Thoughts
Angular 17’s Signals offer a cleaner, simpler alternative to RxJS for many local state needs. While you don’t have to rip out NgRx from your large apps, it’s worth re-evaluating your use of RxJS for basic component state.
- ✅ Reduce boilerplate
- ✅ Improve readability
- ✅ Optimize performance with fine-grained reactivity
Curious about performance? Check out the official Angular Signals performance comparison.
Try It Yourself
Explore a working demo on StackBlitz.
Have you started using Signals in your apps? Are you replacing NgRx for component state? Let’s chat in the comments!

