Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions skills/dev-skills/angular-developer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ When managing state and data reactivity, use Angular Signals and consult the fol
- **Dependent State (`linkedSignal`)**: Creating writable state linked to source signals. Read [linked-signal.md](references/linked-signal.md)
- **Async Reactivity (`resource`)**: Fetching asynchronous data directly into signal state. Read [resource.md](references/resource.md)
- **Side Effects (`effect`)**: Logging, third-party DOM manipulation (`afterRenderEffect`), and when NOT to use effects. Read [effects.md](references/effects.md)
- **RxJS and Signals Interoperability**: Bridging the gap between Observables and Signals using `toSignal`, `toObservable`, and `takeUntilDestroyed`. Read [rxjs-interop.md](references/rxjs-interop.md)

## HTTP Communication

Expand Down
157 changes: 157 additions & 0 deletions skills/dev-skills/angular-developer/references/rxjs-interop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# RxJS and Signals Interoperability

Angular provides a unified reactive model by bridging **RxJS** (ideal for asynchronous event streams) and **Signals** (ideal for synchronous application state and template rendering).

Use the `@angular/core/rxjs-interop` package to convert between the two models and manage subscriptions cleanly.

---

## Converting Observables to Signals (`toSignal`)

Use `toSignal` to read values from an Observable as a reactive Signal. This allows you to bind asynchronous streams directly to templates without using the `async` pipe.

```ts
import {Component, inject} from '@angular/core';
import {toSignal} from '@angular/core/rxjs-interop';
import {UserService} from './user.service';

@Component({
selector: 'app-user-profile',
template: `
@if (user()) {
<p>Welcome, {{ user()?.name }}</p>
}
`,
})
export class UserProfile {
private readonly userService = inject(UserService);

// Convert the Observable stream to a read-only Signal
readonly user = toSignal(this.userService.getCurrentUser());
}
```

- **Subscription Management**: `toSignal` automatically subscribes to the Observable immediately and unsubscribes when the containing component or service is destroyed.
- **Initial Value**: By default, the resulting signal returns `undefined` before the Observable emits its first value. Use the `initialValue` option to set a default:
```ts
readonly user = toSignal(this.userService.getCurrentUser(), {initialValue: {name: 'Guest'}});
```
- **Synchronous Observables**: If the Observable emits synchronously upon subscription (e.g. a `BehaviorSubject`), pass `{ requireSync: true }` to avoid an `undefined` initial type:
```ts
readonly theme = toSignal(this.themeService.theme$, {requireSync: true});
```
- **Error Handling**: If the Observable emits an error, reading the signal will throw that error. You can catch the error using standard try/catch or an error boundary.

---

## Converting Signals to Observables (`toObservable`)

Use `toObservable` to track changes to a Signal and pipe them into RxJS operators. This is highly useful for reacting to state changes and triggering asynchronous operations (like search auto-complete).

```ts
import {Component, inject, signal} from '@angular/core';
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
import {debounceTime, distinctUntilChanged, switchMap} from 'rxjs/operators';
import {SearchService} from './search.service';

@Component({
selector: 'app-search',
template: `
<input (input)="query.set($any($event.target).value)" />
<ul>
@for (item of results(); track item.id) {
<li>{{ item.name }}</li>
}
</ul>
`,
})
export class SearchComponent {
private readonly searchService = inject(SearchService);

readonly query = signal('');

// 1. Convert signal query to an Observable
// 2. Debounce and switchMap to fetch data
// 3. Convert back to a Signal for template binding
readonly results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.searchService.search(q))
),
{initialValue: []}
);
}
```

- **Execution Timing**: `toObservable` uses an `effect` internally to monitor changes. This means emissions are asynchronous and coalesced into microtask timings.
- **Injection Context**: `toObservable` must run in an injection context (such as a constructor or field initializer) unless you explicitly pass an `Injector`.

---

## Automatic Unsubscription (`takeUntilDestroyed`)

Use `takeUntilDestroyed` to automatically complete an Observable stream and clean up subscriptions when the active component, directive, or service is destroyed.

### Inside an Injection Context

When called inside a constructor or property initializer, `takeUntilDestroyed` automatically resolves the current `DestroyRef`.

```ts
import {Component, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {NavigationEnd, Router} from '@angular/router';
import {filter} from 'rxjs/operators';

@Component({
selector: 'app-analytics',
template: `...`,
})
export class AnalyticsComponent {
private readonly router = inject(Router);

constructor() {
// Subscription will automatically be cleaned up on component destroy
this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
takeUntilDestroyed()
).subscribe((event) => {
this.trackPageView(event.url);
});
}

private trackPageView(url: string) { /* ... */ }
}
```

### Outside an Injection Context

If you use `takeUntilDestroyed` inside helper methods or lifecycle hooks, you must explicitly inject and pass `DestroyRef`.

```ts
import {Component, DestroyRef, inject, OnInit} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {interval} from 'rxjs';

@Component({
selector: 'app-timer',
template: `...`,
})
export class TimerComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);

ngOnInit() {
interval(1000).pipe(
takeUntilDestroyed(this.destroyRef) // Explicitly pass the destroy reference
).subscribe((val) => console.log(val));
}
}
```

---

## Recommended Patterns

- **State vs Streams**: Use **Signals** for synchronous UI state, data bindings, and derived values. Use **RxJS** for asynchronous events, timers, HTTP calls, WebSockets, or multi-step coordination pipelines.
- **Async in Templates**: Prefer converting Observables to Signals with `toSignal` instead of subscribing manually or using the `async` pipe.
- **Clean Pipelines**: Do not write to signals inside an RxJS `tap` operator if you can avoid it; instead, use `toSignal` to derive the state.