Angular Signals: A Comprehensive Guide and Walkthrough
Exploring Angular Signals: Understanding Their Purpose, Pros, and Cons for Modern Development
Learn everything about Angular Signals, including their role, pros, cons, and how they compare to RxJS. This comprehensive guide will help developers navigate this new feature in Angular.
Jay McBride
Software Engineer
Introduction
I deleted 300 lines of RxJS boilerplate from a component and replaced it with 40 lines of Angular Signals. The functionality stayed identical. The bugs disappeared.
The Angular team didn’t build Signals to kill RxJS. They built it because they watched developers spend hours debugging subscription leaks, managing async pipes, and fighting change detection. Most reactive state doesn’t need the power of RxJS—it needs simplicity.
This article is for Angular developers who’ve spent an afternoon tracking down a subscription leak or debugging why their component didn’t update. If you haven’t felt the pain of takeUntil patterns and manual unsubscription, come back after you’ve shipped a few production Angular apps.
Enjoying this? 👉 Tip a coffee and keep posts coming
Here’s what Signals actually solve—and when they’re genuinely the wrong tool.
The Core Judgment: Signals for State, RxJS for Streams
After shipping multiple Angular apps with both approaches, here’s my default: use Signals for synchronous state management, use RxJS when you’re actually dealing with asynchronous data streams.
Not theoretical. Specific.
Most developers reach for RxJS because it’s what Angular taught them. Component state becomes a BehaviorSubject. Form inputs become Observables. Everything gets piped through operators that add complexity without solving actual problems.
Signals fix this. They’re synchronous, they’re simple, and they handle the 80% use case that doesn’t need streams.
The question isn’t “should I replace RxJS?” It’s “do I actually have a stream, or do I just have state?”
If your data arrives once and changes occasionally (form values, toggle states, counters), that’s state. Use Signals. If your data arrives continuously over time (WebSocket messages, infinite scroll, HTTP polling), that’s a stream. Use RxJS.
I see teams treat every variable like a stream because RxJS is familiar. Then they wonder why simple features require managing subscriptions, memory leaks, and race conditions.
How This Works in Production
The Signals Mental Model
Signals are reactive variables that notify dependents when they change. That’s it.
You call signal(initialValue) to create one. You call mySignal() to read it. You call mySignal.set(newValue) to update it. Angular’s change detection watches these automatically.
No subscriptions. No async pipes. No manual cleanup. The framework handles it.
What Actually Breaks With RxJS
I maintained an Angular dashboard with 50+ components. Every component used RxJS for local state. Subscriptions everywhere. The pattern looked like this:
export class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
count$ = new BehaviorSubject<number>(0);
ngOnInit() {
this.count$
.pipe(takeUntil(this.destroy$))
.subscribe(count => {
// Update something
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Every component needed this boilerplate. New developers forgot takeUntil and leaked subscriptions. Memory usage crept up. Performance degraded.
With Signals:
export class MyComponent {
count = signal(0);
increment() {
this.count.set(this.count() + 1);
}
}
No lifecycle hooks. No memory leaks. No subscriptions. Angular tracks the Signal automatically and updates the DOM when it changes.
The template just calls count() directly. Angular’s new change detection system watches it and updates efficiently.
A Real Example: Form State Management
I rebuilt a complex multi-step form that was using RxJS for everything. The original implementation had 15 BehaviorSubjects tracking form steps, validation states, and UI flags.
Original RxJS approach:
- 15 BehaviorSubjects
- 30+ subscriptions across components
- Complex combineLatest for computing derived state
- Constant subscription leak bugs from missing takeUntil
- 400+ lines of state management code
Signals rebuild:
// State
currentStep = signal(0);
formData = signal<FormData>({ name: '', email: '', ... });
isValid = computed(() => this.validateStep(this.currentStep()));
// Derived state automatically updates
canProceed = computed(() =>
this.isValid() && !this.isSubmitting()
);
// Updates are simple
nextStep() {
this.currentStep.set(this.currentStep() + 1);
}
Total code: 120 lines. Zero subscription management. Memory leaks: gone. Bugs: dramatically reduced.
The computed() function creates derived Signals that automatically recalculate when dependencies change. No operators. No managing subscription lifetimes. Just declare the dependency graph and Angular handles it.
Common Mistakes I Keep Seeing
Treating Signals like Observables. Signals are synchronous. You can’t map them through operators or compose them with async operations. If you try to force async behavior into Signals, you’re using the wrong tool.
Converting everything to Signals immediately. Your existing RxJS code works. Don’t rewrite working code just because Signals exist. Add Signals to new features. Refactor problem areas that leak subscriptions. Leave stable RxJS code alone.
Missing computed Signals. If you’re manually updating multiple Signals when one changes, you’re doing it wrong. Use computed() to derive values automatically.
// Wrong
firstName = signal('John');
lastName = signal('Doe');
fullName = signal('John Doe');
updateName(first: string, last: string) {
this.firstName.set(first);
this.lastName.set(last);
this.fullName.set(`${first} ${last}`); // Manual update
}
// Right
firstName = signal('John');
lastName = signal('Doe');
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
updateName(first: string, last: string) {
this.firstName.set(first);
this.lastName.set(last);
// fullName updates automatically
}
Not using effect() for side effects. When you need to react to Signal changes with side effects (logging, analytics, external API calls), use effect(). Don’t try to hack subscriptions back in.
Tradeoffs and When Signals Are Wrong
Signals Don’t Handle Async
You can’t await a Signal. You can’t pipe it through operators that deal with time (debounceTime, throttleTime). You can’t handle backpressure or cancellation.
If your data source is async (HTTP requests, WebSockets, timers), wrap it in RxJS and expose the result as a Signal if needed:
// HTTP request with RxJS, exposed as Signal
private userService = inject(UserService);
user = toSignal(
this.userService.getUser(123),
{ initialValue: null }
);
The toSignal() function converts Observables to Signals. Use it at the boundary between async data sources and synchronous component state.
Complex Event Streams Need RxJS
If you’re building a real-time trading dashboard, a chat application, or anything with continuous streams of events that need transformation, filtering, or combining—use RxJS. Signals handle state snapshots, not event streams.
Interop With RxJS Isn’t Perfect Yet
Angular’s ecosystem was built on RxJS. Third-party libraries, HTTP client, router—everything returns Observables. You’ll need to convert between Signals and Observables frequently.
The Angular team provided toSignal() and toObservable() utilities, but you’re still managing two paradigms. This adds cognitive load.
Best Practices I Actually Follow
Use Signals for component state. UI flags, form state, local data—anything that changes synchronously and doesn’t involve async operations.
Use computed() aggressively. Don’t manually update derived values. Let Angular compute them automatically based on dependencies.
Keep RxJS for async boundaries. HTTP calls, WebSocket connections, router events—these should stay as Observables. Convert to Signals at the component boundary if needed.
Use effect() sparingly. Most reactive logic should live in computed Signals. Reserve effect() for actual side effects like logging, analytics, or calling external APIs.
Test Signal logic easily. Signals are just functions. You can test them without Angular’s testing utilities:
it('increments counter', () => {
const count = signal(0);
count.set(count() + 1);
expect(count()).toBe(1);
});
When to Choose Signals vs. RxJS
Choose Signals when:
- Managing local component state
- Handling synchronous updates
- You want automatic change detection
- You’re tired of subscription management
- Your team struggles with RxJS complexity
Choose RxJS when:
- Dealing with asynchronous data streams
- You need operators like debounce, throttle, or switchMap
- Working with continuous event streams
- Combining multiple async sources
- You need fine-grained control over timing and cancellation
Use both when:
- Fetching data async (RxJS) but displaying it synchronously (Signals)
- Building complex features with both state and streams
- Gradually migrating from RxJS to Signals
The honest answer: most component state should use Signals. Most data fetching should use RxJS. Convert between them at the boundaries.
Real Limitations I’ve Hit in Production
TypeScript inference isn’t perfect. Sometimes you need explicit types because the compiler can’t infer Signal generics properly. This is annoying but fixable.
Computed Signals can’t be async. If your derived value requires async computation, you’re stuck using RxJS or managing the async call separately.
Effect() runs in unexpected contexts. Effects run during change detection, which means they can trigger additional change detection cycles if not careful. Keep effects simple and side-effect only.
Debugging is different. RxJS has rxjs-spy and marble testing. Signals have… console.log(). The debugging story is less mature.
Conclusion
Signals solve a real problem: managing synchronous reactive state without the complexity of streams.
They don’t replace RxJS. They complement it by handling the common case that doesn’t need RxJS’s power. Form inputs, toggles, counters, local UI state—these don’t need subscriptions and operators.
The future of Angular isn’t “Signals vs RxJS.” It’s using Signals for state and RxJS for streams.
Start with Signals for new component state. Leave your RxJS HTTP calls alone. Convert Observables to Signals at the component boundary. Test both approaches on real features before committing to a rewrite.
Your users don’t care about your reactive framework. They care about features that work without bugs.
What are your thoughts on Angular Signals? How do you see them fitting into your projects? Share your opinions and experiences in the comments below!
