Migrate Angular Observables to Signals in 20 Minutes with Codeium

Convert legacy RxJS Observables to Angular 19 Signals using Codeium AI assistance. Reduce boilerplate and improve change detection performance.

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 async pipes in templates for simple state
  • ngOnDestroy littered with subscription.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 async pipes
  • Unit tests updated to work with signals
  • Change detection strategy optimized (OnPush where 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