Problem: RxJS Observables Create Unnecessary Complexity
Your Angular app uses Observables everywhere, leading to nested subscriptions, memory leaks from forgotten unsubscribes, and verbose async pipes in templates. Angular 19's Signals offer simpler reactivity, but manually refactoring hundreds of components seems overwhelming.
You'll learn:
- When to migrate Observables to Signals (and when not to)
- How Codeium accelerates the conversion process
- Production-safe patterns for mixed Observable/Signal codebases
Time: 20 min | Level: Intermediate
Why This Happens
Observables were Angular's original reactive primitive, designed for asynchronous streams. But most component state is synchronous—user input, API response data, computed values. Signals provide simpler reactivity for these cases with automatic dependency tracking and better performance.
Common symptoms:
- Multiple
asyncpipes in templates for simple state ngOnDestroylittered withsubscription.unsubscribe()- Change detection running on unrelated updates
- Difficult to trace data flow through nested
combineLatest
When Observables still win:
- HTTP requests (keep using HttpClient)
- WebSocket streams
- Complex event sequences (drag-and-drop, debouncing)
- Existing RxJS operators pipelines
Solution
Step 1: Install Codeium Extension
Codeium provides context-aware code generation that understands Angular patterns.
# VS Code
code --install-extension Codeium.codeium
# Or install from VS Code marketplace
# Search: "Codeium: AI Coding Autocomplete"
Expected: Codeium icon appears in sidebar, prompts for login/signup.
Configure for Angular:
- Open Codeium settings (Cmd/Ctrl + Shift + P → "Codeium: Settings")
- Enable "Framework-specific suggestions"
- Set language to TypeScript
Step 2: Identify Migration Candidates
Not all Observables should become Signals. Use this decision tree:
// ✅ MIGRATE: Simple component state
@Component({...})
export class UserProfile {
// Before: Observable
userName$ = new BehaviorSubject<string>('');
// After: Signal
userName = signal('');
}
// ❌ KEEP: Asynchronous streams
@Component({...})
export class ChatComponent {
// Keep Observable for WebSocket events
messages$ = this.chatService.messages$;
}
// ✅ MIGRATE: Computed values
@Component({...})
export class CartComponent {
// Before: Observable with combineLatest
total$ = combineLatest([this.items$, this.tax$]).pipe(
map(([items, tax]) => calculateTotal(items, tax))
);
// After: Computed signal
total = computed(() =>
calculateTotal(this.items(), this.tax())
);
}
Rule of thumb: If you're using BehaviorSubject, Subject for state, or combineLatest for derived values, migrate. Keep Observables for HTTP and event streams.
Step 3: Use Codeium to Convert BehaviorSubjects
Select a BehaviorSubject declaration and use Codeium's chat:
Prompt to Codeium:
Convert this BehaviorSubject to an Angular 19 signal, preserving all functionality
Before:
export class ProductList {
private _products$ = new BehaviorSubject<Product[]>([]);
products$ = this._products$.asObservable();
addProduct(product: Product) {
const current = this._products$.value;
this._products$.next([...current, product]);
}
ngOnDestroy() {
this._products$.complete();
}
}
After (Codeium-generated):
export class ProductList {
products = signal<Product[]>([]);
addProduct(product: Product) {
this.products.update(current => [...current, product]);
}
// No ngOnDestroy needed - signals auto-cleanup
}
Why this works: Signals use .update() for transformations, eliminating the Subject pattern. No manual subscription management needed.
Step 4: Convert Template Async Pipes
Templates with multiple async pipes cause duplicate subscriptions. Codeium can batch-convert these.
Select your template code, prompt:
Replace async pipes with signal syntax
Before:
<div class="user-profile">
<h1>{{ (userName$ | async) || 'Guest' }}</h1>
<p>Items: {{ (cartCount$ | async) }}</p>
<span *ngIf="isLoading$ | async">Loading...</span>
</div>
After (Codeium-generated):
<div class="user-profile">
<h1>{{ userName() || 'Guest' }}</h1>
<p>Items: {{ cartCount() }}</p>
<span *ngIf="isLoading()">Loading...</span>
</div>
Performance gain: One change detection cycle instead of three. Signals only update when values actually change.
Step 5: Refactor Computed Values
Complex combineLatest chains become simple computed() calls.
Prompt Codeium with context:
Convert this combineLatest observable to a computed signal
Before:
readonly filteredProducts$ = combineLatest([
this.products$,
this.searchTerm$,
this.category$
]).pipe(
map(([products, term, category]) =>
products.filter(p =>
p.name.includes(term) && p.category === category
)
)
);
After:
readonly filteredProducts = computed(() => {
const products = this.products();
const term = this.searchTerm();
const category = this.category();
return products.filter(p =>
p.name.includes(term) && p.category === category
);
});
Why this is better: Dependencies automatically tracked. No manual operator chains. Easier to debug—just put a breakpoint in the computed function.
Step 6: Handle HTTP Requests (Keep Observables)
Don't convert HttpClient Observables. Instead, bridge them with toSignal():
import { toSignal } from '@angular/core/rxjs-interop';
@Component({...})
export class UserDashboard {
private http = inject(HttpClient);
// Keep HTTP as Observable
private userData$ = this.http.get<User>('/api/user');
// Convert to signal for template use
userData = toSignal(this.userData$, { initialValue: null });
// Computed signal based on HTTP data
displayName = computed(() => {
const user = this.userData();
return user ? `${user.firstName} ${user.lastName}` : 'Loading...';
});
}
Template:
<h1>Welcome, {{ displayName() }}</h1>
If it fails:
- Error: "toSignal() requires initialValue": Provide
{ initialValue: null }or{ requireSync: true }if data is guaranteed - Data doesn't update: Check that the Observable actually emits. Use
shareReplay(1)if needed
Step 7: Use Codeium for Effect Migration
Side effects in subscribe() blocks become effect() calls.
Before:
export class AnalyticsTracker {
ngOnInit() {
this.currentPage$.subscribe(page => {
console.log('Page view:', page);
this.analytics.track(page);
});
}
}
Prompt Codeium:
Convert this subscription to an Angular effect
After:
export class AnalyticsTracker {
constructor() {
effect(() => {
const page = this.currentPage();
console.log('Page view:', page);
this.analytics.track(page);
});
}
}
Effect runs automatically when currentPage signal changes. No subscription management.
Important limitation: Effects run in the Angular zone. Don't use for heavy computations—use computed() instead.
Verification
Test Change Detection Performance
// Add to your component
import { ChangeDetectorRef } from '@angular/core';
export class TestComponent {
private cdRef = inject(ChangeDetectorRef);
constructor() {
// Check how often change detection runs
effect(() => {
console.log('Signal changed:', this.mySignal());
});
}
}
Expected behavior: Console logs only when mySignal actually changes, not on every Angular tick.
Check for Memory Leaks
# Build production bundle
ng build --configuration production
# Check bundle size reduction
ls -lh dist/your-app/main.*.js
You should see: 5-15% smaller bundle (fewer RxJS operators) and no ngOnDestroy unsubscribe boilerplate.
Codeium Pro Tips
1. Batch Convert Files
Select entire component file, use Codeium Command (Cmd/Ctrl + I):
Migrate all BehaviorSubjects and Subjects in this file to signals.
Keep HTTP observables. Update template to remove async pipes.
Codeium will generate a diff showing all changes. Review before accepting.
2. Generate Signal Utilities
Create a utility function to convert an Observable to a writable signal
that updates when the Observable emits
Codeium generates:
export function toWritableSignal<T>(
observable: Observable<T>,
initialValue: T
): WritableSignal<T> {
const signal = signal<T>(initialValue);
observable.subscribe(value => signal.set(value));
return signal;
}
3. Context-Aware Suggestions
When typing computed(, Codeium suggests dependencies based on nearby signals:
export class PricingComponent {
price = signal(100);
quantity = signal(1);
taxRate = signal(0.08);
// Start typing: total = computed(() =>
// Codeium suggests:
total = computed(() =>
this.price() * this.quantity() * (1 + this.taxRate())
);
}
What You Learned
- Signals simplify 80% of component state management
- Keep Observables for HTTP and event streams
toSignal()bridges the gap for async operations- Codeium accelerates migration with pattern-aware generation
Limitations:
- Signals are synchronous—not suitable for complex async flows
- Older Angular libraries may not support Signals yet
- Team needs to understand when to use each primitive
Migration strategy for large apps:
- Convert new components to Signals
- Refactor existing components incrementally
- Keep shared services as Observables initially
- Full migration can take 2-3 sprints for 50+ components
Production Checklist
Before deploying Signal-based code:
- All HTTP requests still use Observables
- Form controls use
toSignal()for reactive forms - No
effect()calls with heavy computations - Templates removed all unnecessary
asyncpipes - Unit tests updated to work with signals
- Change detection strategy optimized (
OnPushwhere possible)
Common gotcha: Signals don't trigger change detection outside Angular zone. If updating from setTimeout or third-party callbacks, wrap in NgZone.run():
constructor(private zone: NgZone) {
someThirdPartyLib.onEvent(() => {
this.zone.run(() => {
this.mySignal.set(newValue); // Now triggers change detection
});
});
}
Tested on Angular 19.1, Codeium 1.8.44, TypeScript 5.5, Node.js 22.x