Skip to content
Open
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
298 changes: 298 additions & 0 deletions adev/src/content/ecosystem/rxjs-interop/signals-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,301 @@ export class UserProfile {
The `stream` property accepts a factory function for an RxJS `Observable`. This factory function is passed the resource's `params` value and returns an `Observable`. The resource calls this factory function every time the `params` computation produces a new value. See [Resource loaders](/guide/signals/resource#resource-loaders) for more details on the parameters passed to the factory function.

In all other ways, `rxResource` behaves like and provides the same APIs as `resource` for specifying parameters, reading values, checking loading state, and examining errors.

# Migrating common RxJS patterns to signals

If you're moving from an RxJS-centric codebase, the following patterns show signal-based equivalents for common observable idioms.

> Signals are not a complete replacement for RxJS.
> RxJS remains the preferred solution for complex asynchronous event streams, advanced operators, and stream orchestration.

## `BehaviorSubject` → `signal`

A `BehaviorSubject` is the closest equivalent to a writable signal — both hold a current value and notify consumers on change.

### Before (RxJS)

```ts
import {BehaviorSubject} from 'rxjs';

export class CounterService {
private _count = new BehaviorSubject(0);

readonly count$ = this._count.asObservable();

increment() {
this._count.next(this._count.getValue() + 1);
}
}
````

### After (Signals)

```ts
import {signal} from '@angular/core';

export class CounterService {
private _count = signal(0);

readonly count = this._count.asReadonly();

increment() {
this._count.update(v => v + 1);
}
}
```

### Why use signals here?

* Simpler API surface
* No subscriptions required
* Automatic dependency tracking
* Better template integration

---

## `combineLatest` → `computed`

`combineLatest` combines multiple observable streams into one.

`computed` derives reactive state from other signals with automatic dependency tracking.

### Before (RxJS)

```ts
import {combineLatest, map} from 'rxjs';

readonly summary$ = combineLatest([
this.firstName$,
this.lastName$
]).pipe(
map(([first, last]) => `${first} ${last}`)
);
```

### After (Signals)

```ts
import {computed} from '@angular/core';

readonly summary = computed(
() => `${this.firstName()} ${this.lastName()}`
);
```

### Benefits

* Automatic dependency tracking
* Memoized derived state
* No manual subscriptions
* Cleaner reactive code

---

## Observable → `toSignal`

Use `toSignal()` to bridge existing RxJS streams into the signal ecosystem.

This is especially useful when integrating legacy services or third-party observable APIs.

### Before (RxJS)

```ts
readonly user$ = this.userService.user$;
```

### After (Signals)

```ts
import {toSignal} from '@angular/core/rxjs-interop';

readonly user = toSignal(this.userService.user$, {
initialValue: null,
});
```

### Notes

* `toSignal()` automatically subscribes to the Observable
* Cleanup happens automatically on destroy
* An `initialValue` may be required for async streams

---

## `switchMap` + HTTP → `resource` / `rxResource`

`switchMap` is commonly used to re-fetch data when a source value changes.

Consider using `resource` or `rxResource` for signal-based async data handling.

### Before (RxJS)

```ts
import {switchMap} from 'rxjs';

readonly user$ = this.userId$.pipe(
switchMap(id =>
this.http.get<User>(`/api/users/${id}`)
)
);
```

### After (Signals)

```ts
import {resource, input} from '@angular/core';

readonly userId = input<string>();

readonly userResource = resource({
params: () => ({
id: this.userId(),
}),

loader: ({params}) =>
fetch(`/api/users/${params.id}`)
.then(r => r.json()),
});
```

### Accessing resource state

```ts
userResource.value()
userResource.isLoading()
userResource.error()
```

### Benefits

* Built-in loading state
* Built-in error handling
* Automatic re-fetching
* Signal-native async patterns

---

## `distinctUntilChanged` → signal equality

Signals only notify consumers when the value changes using `Object.is` by default.

Provide a custom `equal` function for object values — similar to `distinctUntilChanged`.

### Before (RxJS)

```ts
import {distinctUntilChanged} from 'rxjs';

readonly activeUser$ = this.user$.pipe(
distinctUntilChanged(
(a, b) => a.id === b.id
)
);
```

### After (Signals)

```ts
import {signal} from '@angular/core';

readonly activeUser = signal<User | null>(
null,
{
equal: (a, b) => a?.id === b?.id,
}
);
```

### Benefits

* Prevents unnecessary recomputation
* Avoids redundant DOM updates
* Reduces effect re-execution

---

## `debounceTime` → RxJS interop with signals

Use `toObservable()` when RxJS operators such as `debounceTime` are still needed.

### Before (RxJS)

```ts
import {debounceTime, switchMap} from 'rxjs';

readonly results$ = this.query$.pipe(
debounceTime(300),
switchMap(query =>
this.http.get(`/search?q=${query}`)
)
);
```

### After (Signals + RxJS interop)

```ts
import {signal} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';

import {
debounceTime,
switchMap,
} from 'rxjs';

readonly query = signal('');

readonly results$ = toObservable(this.query).pipe(
debounceTime(300),

switchMap(query =>
this.http.get(`/search?q=${query}`)
)
);
```

### Why keep RxJS here?

RxJS operators remain powerful for:

* debouncing
* throttling
* retries
* cancellation
* stream composition

Signals and RxJS work best together rather than replacing one another completely.

---

## `takeUntilDestroyed` → automatic cleanup

Signals and resources clean up automatically when the component or service that created them is destroyed.

Manual unsubscription is often unnecessary.

### Before (RxJS)

```ts
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

this.user$
.pipe(takeUntilDestroyed())
.subscribe();
```

### After (Signals)

```ts
readonly user = toSignal(this.user$);
```

### Benefits

* Automatic lifecycle cleanup
* Less boilerplate
* Reduced memory leak risk

> If you still need to use `toSignal()` with an existing Observable, the resulting signal also unsubscribes automatically on destroy.

```
```
Loading