Skip to content
ToolShedby Wasim Shaikh

RxJS to Signals Reference

Side-by-side RxJS to Angular signals reference: toSignal, httpResource, rxResource, resource, linkedSignal and takeUntilDestroyed, type-checked against Angular 22.

Showing 40 of 40 mappings

rxjs / rxjs/operators

@angular/core + rxjs-interopSetup
RxJS
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
Signals
import { signal, computed, effect, linkedSignal, resource } from '@angular/core';
import { toSignal, toObservable, rxResource, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { httpResource } from '@angular/common/http';

Signal primitives and resource() come from @angular/core. Everything that touches an Observable lives in @angular/core/rxjs-interop, and httpResource() sits next to HttpClient.

Source: RxJS interop with Angular signals (angular.dev)

async pipe

toSignal()Setup
RxJS
private userService = inject(UserService);
user$ = this.userService.user$;

<!-- template -->
@if (user$ | async; as user) {
  <p>{{ user.name }}</p>
}
Signals
private userService = inject(UserService);
user = toSignal(this.userService.user$);

<!-- template -->
@if (user(); as user) {
  <p>{{ user.name }}</p>
}

toSignal() subscribes straight away and unsubscribes when the component is destroyed, so it must run in an injection context (a field initializer or the constructor). Without an initial value the type is Signal<User | undefined>.

Source: RxJS interop with Angular signals (angular.dev)

startWith(value)

toSignal(obs$, { initialValue })Reading streams
RxJS
private counter = inject(CounterService);
count$ = this.counter.count$.pipe(startWith(0));
Signals
private counter = inject(CounterService);
count = toSignal(this.counter.count$, { initialValue: 0 });

The signal returns initialValue until the Observable first emits, and the type drops undefined: Signal<number>.

Source: RxJS interop with Angular signals (angular.dev)

BehaviorSubject (emits on subscribe)

toSignal(obs$, { requireSync: true })Reading streams
RxJS
private cart = inject(CartStore);
items$ = this.cart.items$; // backed by a BehaviorSubject

<!-- template -->
@for (item of (items$ | async) ?? []; track item.id) {
  <li>{{ item.name }}</li>
}
Signals
private cart = inject(CartStore);
items = toSignal(this.cart.items$, { requireSync: true }); // Signal<Item[]>

<!-- template -->
@for (item of items(); track item.id) {
  <li>{{ item.name }}</li>
}

Use requireSync for sources that emit synchronously on subscribe, such as a BehaviorSubject or anything piped through startWith(). The type has no undefined, and toSignal throws at runtime if the source does not emit right away.

Source: RxJS interop with Angular signals (angular.dev)

subscribe() in ngOnInit

toSignal(obs$, { injector })Reading streams
RxJS
@Input() id!: string;
user$!: Observable<User>;
private users = inject(UserService);

ngOnInit() {
  this.user$ = this.users.getUser(this.id);
}
Signals
id = input.required<string>();
user!: Signal<User | undefined>;
private users = inject(UserService);
private injector = inject(Injector);

ngOnInit() {
  this.user = toSignal(this.users.getUser(this.id()), { injector: this.injector });
}

Calling toSignal() outside an injection context throws NG0203. Pass an injector if you must create it later, but when the Observable depends on an input, rxResource() (see rxResource & resource) is usually the better fit because it re-runs when the input changes.

Source: RxJS interop with Angular signals (angular.dev)

subscription that outlives the component

toSignal(obs$, { manualCleanup: true })Reading streams
RxJS
config?: Config;

ngOnInit() {
  this.http.get<Config>('/api/config').subscribe((config) => (this.config = config));
}
Signals
config = toSignal(this.http.get<Config>('/api/config'), {
  manualCleanup: true,
});

By default toSignal() unsubscribes when its injection context is destroyed. manualCleanup keeps the subscription until the Observable completes on its own, which suits one-shot sources like an HTTP GET.

Source: RxJS interop with Angular signals (angular.dev)

BehaviorSubject store

signal() + asReadonly()State
RxJS
@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly items$$ = new BehaviorSubject<Item[]>([]);
  readonly items$ = this.items$$.asObservable();

  add(item: Item) {
    this.items$$.next([...this.items$$.value, item]);
  }
}
Signals
@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly _items = signal<Item[]>([]);
  readonly items = this._items.asReadonly();

  add(item: Item) {
    this._items.update((items) => [...items, item]);
  }
}

A writable signal replaces the BehaviorSubject, and asReadonly() replaces asObservable(). Readers call cartStore.items() with no subscription, and the current value is always available.

Source: Angular Signals (angular.dev)

subject.next(value)

signal.set(value)State
RxJS
private filter$ = new BehaviorSubject<'all' | 'open' | 'done'>('all');

setFilter(filter: 'all' | 'open' | 'done') {
  this.filter$.next(filter);
}
Signals
filter = signal<'all' | 'open' | 'done'>('all');

setFilter(filter: 'all' | 'open' | 'done') {
  this.filter.set(filter);
}

set() replaces the value and update() derives it from the old one. Setting the same value (by Object.is) notifies nobody.

Source: Angular Signals (angular.dev)

scan()

signal.update()State
RxJS
private add$ = new Subject<number>();
total$ = this.add$.pipe(scan((sum, n) => sum + n, 0));

add(n: number) {
  this.add$.next(n);
}
Signals
total = signal(0);

add(n: number) {
  this.total.update((sum) => sum + n);
}

The accumulator moves into update(). The seed becomes the signal’s initial value.

Source: Angular Signals (angular.dev)

pipe(map())

computed()Derived values
RxJS
private cart = inject(CartStore);

total$ = this.cart.items$.pipe(
  map((items) => items.reduce((sum, i) => sum + i.price, 0)),
);
Signals
private cart = inject(CartStore);

total = computed(() =>
  this.cart.items().reduce((sum, i) => sum + i.price, 0),
);

computed() is lazy and memoized: it runs on first read and again only after a signal it read has changed.

Source: Angular Signals (angular.dev)

combineLatest()

computed()Derived values
RxJS
todos$ = new BehaviorSubject<Todo[]>([]);
filter$ = new BehaviorSubject<'all' | 'open' | 'done'>('all');

visible$ = combineLatest([this.todos$, this.filter$]).pipe(
  map(([todos, filter]) =>
    todos.filter((t) => filter === 'all' || t.done === (filter === 'done')),
  ),
);
Signals
todos = signal<Todo[]>([]);
filter = signal<'all' | 'open' | 'done'>('all');

visible = computed(() => {
  const filter = this.filter();
  return this.todos().filter((t) => filter === 'all' || t.done === (filter === 'done'));
});

There is no list of sources to keep in sync: computed() tracks every signal it reads. No glitches either, as a change to both signals produces one recomputation.

Source: Angular Signals (angular.dev)

distinctUntilChanged()

computed(fn, { equal })Derived values
RxJS
user$ = new BehaviorSubject<User>(guest);

address$ = this.user$.pipe(
  map((user) => user.address),
  distinctUntilChanged((a, b) => a.city === b.city),
);
Signals
user = signal<User>(guest);

address = computed(() => this.user().address, {
  equal: (a, b) => a.city === b.city,
});

Signals already skip values that are Object.is-equal, so a bare distinctUntilChanged() needs no replacement. Pass equal for a custom comparison; signal(), toSignal(), linkedSignal() and the resource APIs accept it too.

Source: Angular Signals (angular.dev)

shareReplay(1)

httpResource() in a serviceDerived values
RxJS
@Injectable({ providedIn: 'root' })
export class SettingsService {
  private http = inject(HttpClient);
  readonly settings$ = this.http.get<Settings>('/api/settings').pipe(shareReplay(1));
}
Signals
@Injectable({ providedIn: 'root' })
export class SettingsService {
  readonly settings = httpResource<Settings>(() => '/api/settings');
}

Signals hold their latest value, so there is nothing to replay. A resource in a root service loads once, and every component that reads settings.value() shares that result.

Source: Reactive data fetching with httpResource (angular.dev)

subscribe() for side effects

effect()Side effects
RxJS
theme$ = new BehaviorSubject<'light' | 'dark'>('light');

ngOnInit() {
  this.theme$.pipe(takeUntil(this.destroy$)).subscribe((theme) => {
    localStorage.setItem('theme', theme);
  });
}
Signals
theme = signal<'light' | 'dark'>('light');

constructor() {
  effect(() => {
    localStorage.setItem('theme', this.theme());
  });
}

Use effect() to push signal values into non-reactive APIs such as storage, logging or a third-party widget. Don’t use it to copy one signal into another; that is what computed() and linkedSignal() are for.

Source: Effects (angular.dev)

takeUntil(this.destroy$)

takeUntilDestroyed()Side effects
RxJS
private destroy$ = new Subject<void>();

ngOnInit() {
  this.socket.messages$
    .pipe(takeUntil(this.destroy$))
    .subscribe((msg) => this.log.push(msg));
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}
Signals
constructor() {
  this.socket.messages$
    .pipe(takeUntilDestroyed())
    .subscribe((msg) => this.log.push(msg));
}

Without an argument it must run in an injection context. Anywhere else, inject DestroyRef in a field and pass it: takeUntilDestroyed(this.destroyRef).

Source: Unsubscribing with takeUntilDestroyed (angular.dev)

switchMap(() => interval())

effect((onCleanup) => …)Side effects
RxJS
intervalMs$ = new BehaviorSubject(1000);

ngOnInit() {
  this.intervalMs$
    .pipe(switchMap((ms) => interval(ms)), takeUntil(this.destroy$))
    .subscribe(() => this.tick());
}
Signals
intervalMs = input(1000);

constructor() {
  effect((onCleanup) => {
    const id = setInterval(() => this.tick(), this.intervalMs());
    onCleanup(() => clearInterval(id));
  });
}

onCleanup runs before the effect re-runs and when it is destroyed, which gives the same "cancel the previous one" behavior as switchMap.

Source: Effects (angular.dev)

switchMap(() => http.get())

httpResource()httpResource
RxJS
private route = inject(ActivatedRoute);

user$ = this.route.paramMap.pipe(
  map((params) => params.get('id')!),
  switchMap((id) => this.http.get<User>(`/api/users/${id}`)),
);
Signals
id = input.required<string>(); // route param via withComponentInputBinding()

user = httpResource<User>(() => `/api/users/${this.id()}`);

httpResource() sends a GET whenever a signal read in the function changes and cancels the request still in flight, like switchMap. It goes through HttpClient, so interceptors and HttpTestingController still apply.

Source: Reactive data fetching with httpResource (angular.dev)

http.get(url, { params, headers })

httpResource(() => ({ url, params, headers }))httpResource
RxJS
query$ = new BehaviorSubject('');
page$ = new BehaviorSubject(1);

products$ = combineLatest([this.query$, this.page$]).pipe(
  switchMap(([q, page]) =>
    this.http.get<Product[]>('/api/products', {
      params: { q, page },
      headers: { 'X-Client': 'web' },
    }),
  ),
);
Signals
query = signal('');
page = signal(1);

products = httpResource<Product[]>(() => ({
  url: '/api/products',
  params: { q: this.query(), page: this.page() },
  headers: { 'X-Client': 'web' },
}));

Return a request object for anything beyond a plain URL: method, body, params, headers, withCredentials, timeout, transferCache and the fetch options (cache, priority, keepalive and so on).

Source: Reactive data fetching with httpResource (angular.dev)

filter() before switchMap

return undefinedhttpResource
RxJS
query$ = new BehaviorSubject('');

results$ = this.query$.pipe(
  filter((q) => q.length >= 2),
  switchMap((q) => this.http.get<Result[]>('/api/search', { params: { q } })),
);
Signals
query = signal('');

results = httpResource<Result[]>(() => {
  const q = this.query();
  return q.length >= 2 ? { url: '/api/search', params: { q } } : undefined;
});

Returning undefined sends no request and puts the resource in the idle status, where value() is the defaultValue (undefined if you set none). Unlike filter(), the previous results are not kept.

Source: Reactive data fetching with httpResource (angular.dev)

map(validate)

httpResource(…, { parse })httpResource
RxJS
user$ = this.http
  .get<unknown>('/api/me')
  .pipe(map((data) => UserSchema.parse(data)));
Signals
user = httpResource(() => '/api/me', { parse: UserSchema.parse });

parse receives the raw response and its return type becomes the resource type, so a Zod (or similar) schema gives runtime validation and a typed value() in one step. A thrown parse error puts the resource in the error status.

Source: Reactive data fetching with httpResource (angular.dev)

http.get(url, { responseType: 'text' })

httpResource.text() / .blob() / .arrayBuffer()httpResource
RxJS
@Input() id!: string;

readme$ = this.http.get('/README.md', { responseType: 'text' });
avatar$ = this.http.get(`/api/users/${this.id}/avatar`, { responseType: 'blob' });
Signals
id = input.required<string>();

readme = httpResource.text(() => '/README.md');
avatar = httpResource.blob(() => `/api/users/${this.id()}/avatar`);

httpResource() parses JSON. The .text(), .blob() and .arrayBuffer() variants return a string, Blob or ArrayBuffer in value().

Source: Reactive data fetching with httpResource (angular.dev)

reportProgress + observe: 'events'

httpResource.progress()httpResource
RxJS
loaded = 0;

download() {
  this.http
    .get('/files/report.pdf', { responseType: 'blob', reportProgress: true, observe: 'events' })
    .subscribe((event) => {
      if (event.type === HttpEventType.DownloadProgress) {
        this.loaded = event.loaded;
      }
    });
}
Signals
file = httpResource.blob(() => ({
  url: '/files/report.pdf',
  reportProgress: true,
}));

loaded = computed(() => this.file.progress()?.loaded ?? 0);

With reportProgress: true the latest HttpProgressEvent is exposed on the progress signal. It covers download progress only.

Source: Reactive data fetching with httpResource (angular.dev)

observe: 'response'

headers() / statusCode()httpResource
RxJS
items: Item[] = [];
total = 0;

load() {
  this.http.get<Item[]>('/api/items', { observe: 'response' }).subscribe((res) => {
    this.items = res.body ?? [];
    this.total = Number(res.headers.get('X-Total-Count'));
  });
}
Signals
items = httpResource<Item[]>(() => '/api/items', { defaultValue: [] });

total = computed(() => Number(this.items.headers()?.get('X-Total-Count') ?? 0));

An httpResource also exposes the response headers and statusCode as signals, so there is no observe option to switch.

Source: Reactive data fetching with httpResource (angular.dev)

http.post().subscribe()

HttpClient + resource.reload()httpResource
RxJS
private refresh$ = new Subject<void>();

todos$ = this.refresh$.pipe(
  startWith(undefined),
  switchMap(() => this.http.get<Todo[]>('/api/todos')),
);

save(todo: Todo) {
  this.http.post('/api/todos', todo).subscribe(() => this.refresh$.next());
}
Signals
todos = httpResource<Todo[]>(() => '/api/todos', { defaultValue: [] });

save(todo: Todo) {
  this.http.post('/api/todos', todo).subscribe(() => this.todos.reload());
}

Keep POST, PUT and DELETE on HttpClient. Resources are for reads: they cancel in-flight requests when their params change, which could abort a write. Reload the affected resource once the write succeeds.

Source: Reactive data fetching with httpResource (angular.dev)

switchMap to a service Observable

rxResource({ params, stream })rxResource & resource
RxJS
private users = inject(UserService);
id$ = new BehaviorSubject('1');

user$ = this.id$.pipe(switchMap((id) => this.users.getUser(id)));
Signals
private users = inject(UserService);
id = signal('1');

user = rxResource({
  params: () => this.id(),
  stream: ({ params }) => this.users.getUser(params),
});

rxResource() keeps your existing Observable-returning services. The options are params and stream (request and loader in the v19 preview). The Observable must emit or error before completing, or Angular throws NG0991.

Source: RxJS interop with Angular signals (angular.dev)

from(fetch()) / async ngOnInit

resource({ params, loader })rxResource & resource
RxJS
@Input() city!: string;
loading = false;
weather?: Weather;

async ngOnInit() {
  this.loading = true;
  const res = await fetch(`/api/weather?city=${this.city}`);
  this.weather = await res.json();
  this.loading = false;
}
Signals
city = input.required<string>();

weather = resource({
  params: () => ({ city: this.city() }),
  loader: async ({ params, abortSignal }) => {
    const res = await fetch(`/api/weather?city=${params.city}`, { signal: abortSignal });
    return (await res.json()) as Weather;
  },
});

resource() is the Promise-based version for fetch() or any async SDK. Pass abortSignal on so a stale request is cancelled when params change.

Source: Async reactivity with resources (angular.dev)

webSocket().subscribe()

rxResource({ stream })rxResource & resource
RxJS
price?: Price;

ngOnInit() {
  webSocket<Price>('wss://example.com/prices')
    .pipe(takeUntil(this.destroy$))
    .subscribe((p) => (this.price = p));
}
Signals
price = rxResource({
  stream: () => webSocket<Price>('wss://example.com/prices'),
});

Every emission updates price.value(), and the socket closes when the component is destroyed. Without params the stream starts once; add params to reconnect when they change.

Source: RxJS interop with Angular signals (angular.dev)

nested switchMap (dependent requests)

params: ({ chain }) => …rxResource & resource
RxJS
@Input() id!: string;

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

company$ = this.user$.pipe(
  switchMap((user) => this.http.get<Company>(`/api/companies/${user.companyId}`)),
);
Signals
id = input.required<string>();

user = httpResource<User>(() => `/api/users/${this.id()}`);

company = httpResource<Company>(({ chain }) => {
  const companyId = chain(this.user)?.companyId;
  return companyId ? `/api/companies/${companyId}` : undefined;
});

chain() returns the other resource’s value once it has one. Until then, company copies user’s loading or error status instead of sitting idle, so one spinner or error message covers both requests.

Source: Async reactivity with resources (angular.dev)

loading flag + finalize()

isLoading()Loading & errors
RxJS
loading = false;
users: User[] = [];

load() {
  this.loading = true;
  this.http
    .get<User[]>('/api/users')
    .pipe(finalize(() => (this.loading = false)))
    .subscribe((users) => (this.users = users));
}
Signals
users = httpResource<User[]>(() => '/api/users');

<!-- template -->
@if (users.isLoading()) {
  <app-spinner />
}

isLoading() is true in both the loading and reloading statuses.

Source: Async reactivity with resources (angular.dev)

catchError()

error() + hasValue()Loading & errors
RxJS
error = '';

users$ = this.http.get<User[]>('/api/users').pipe(
  catchError((err) => {
    this.error = err.message;
    return of([]);
  }),
);
Signals
users = httpResource<User[]>(() => '/api/users');

<!-- template -->
@if (users.error(); as err) {
  <p>Could not load users: {{ err.message }}</p>
} @else if (users.hasValue()) {
  @for (u of users.value(); track u.id) {
    <li>{{ u.name }}</li>
  }
}

value() throws while the resource is in the error status, so guard reads with hasValue(). It also narrows the type, removing undefined.

Source: Reactive data fetching with httpResource (angular.dev)

state: 'loading' | 'loaded' | 'error'

status()Loading & errors
RxJS
users: User[] = [];
state: 'idle' | 'loading' | 'loaded' | 'error' = 'idle';

load() {
  this.state = 'loading';
  this.http.get<User[]>('/api/users').subscribe({
    next: (users) => { this.users = users; this.state = 'loaded'; },
    error: () => (this.state = 'error'),
  });
}
Signals
users = httpResource<User[]>(() => '/api/users');

<!-- template -->
@switch (users.status()) {
  @case ('loading') { <app-spinner /> }
  @case ('error') { <p>{{ users.error()?.message }}</p> }
  @case ('resolved') { <app-user-list [users]="users.value()!" /> }
}

status() is one of 'idle', 'loading', 'reloading', 'resolved', 'error' or 'local' (set locally with set() or update()).

Source: Async reactivity with resources (angular.dev)

refresh$ Subject + startWith

reload()Loading & errors
RxJS
private refresh$ = new Subject<void>();

orders$ = this.refresh$.pipe(
  startWith(undefined),
  switchMap(() => this.http.get<Order[]>('/api/orders')),
);

refresh() {
  this.refresh$.next();
}
Signals
orders = httpResource<Order[]>(() => '/api/orders');

refresh() {
  this.orders.reload();
}

During a reload the status is 'reloading' and value() keeps the previous data, so the list does not flash empty.

Source: Async reactivity with resources (angular.dev)

startWith([])

{ defaultValue }Loading & errors
RxJS
items$ = this.http.get<Item[]>('/api/items').pipe(startWith([] as Item[]));
Signals
items = httpResource<Item[]>(() => '/api/items', { defaultValue: [] });
// items.value() is Item[], never undefined

defaultValue is returned while the resource is idle or loading and removes undefined from the type. resource() and rxResource() take the same option.

Source: Async reactivity with resources (angular.dev)

BehaviorSubject.next() for a local edit

resource.update()Loading & errors
RxJS
todos$$ = new BehaviorSubject<Todo[]>([]);

toggle(todo: Todo) {
  const next = this.todos$$.value.map((t) =>
    t.id === todo.id ? { ...t, done: !t.done } : t,
  );
  this.todos$$.next(next);
}
Signals
todos = httpResource<Todo[]>(() => '/api/todos', { defaultValue: [] });

toggle(todo: Todo) {
  this.todos.update((list) =>
    list.map((t) => (t.id === todo.id ? { ...t, done: !t.done } : t)),
  );
}

Resources are writable. A local set() or update() moves the status to 'local' until the params change or you call reload().

Source: Async reactivity with resources (angular.dev)

subscribe() to reset a field

linkedSignal()linkedSignal
RxJS
options$ = new BehaviorSubject<Option[]>([]);
selected?: Option;

ngOnInit() {
  this.options$.pipe(takeUntil(this.destroy$)).subscribe((options) => {
    this.selected = options[0];
  });
}
Signals
options = input.required<Option[]>();

selected = linkedSignal(() => this.options()[0]);

choose(option: Option) {
  this.selected.set(option);
}

A linkedSignal is writable like signal(), but it resets to the computation whenever options change. That replaces the effect or subscription people use to "reset on change".

Source: Dependent state with linkedSignal (angular.dev)

withLatestFrom() to keep a selection

linkedSignal({ source, computation })linkedSignal
RxJS
options$ = new BehaviorSubject<Option[]>([]);
selected$$ = new BehaviorSubject<Option | undefined>(undefined);

selected$ = this.options$.pipe(
  withLatestFrom(this.selected$$),
  map(([options, current]) => options.find((o) => o.id === current?.id) ?? options[0]),
);
Signals
options = input.required<Option[]>();

selected = linkedSignal<Option[], Option>({
  source: this.options,
  computation: (options, previous) =>
    options.find((o) => o.id === previous?.value.id) ?? options[0],
});

The long form passes the previous source and value to the computation, so a selection survives a refresh when the item still exists. Give the generic types explicitly when you use previous.

Source: Dependent state with linkedSignal (angular.dev)

@Input() setter feeding a Subject

toObservable()Back to RxJS
RxJS
private query$ = new BehaviorSubject('');

@Input() set query(q: string) {
  this.query$.next(q);
}

suggestions$ = this.query$.pipe(
  debounceTime(300),
  switchMap((q) => this.api.suggest(q)),
);
Signals
query = input('');

suggestions = toSignal(
  toObservable(this.query).pipe(
    debounceTime(300),
    switchMap((q) => this.api.suggest(q)),
  ),
  { initialValue: [] },
);

toObservable() emits from an effect, so it is asynchronous: several synchronous set() calls produce one emission with the last value. Use it when you still need RxJS operators.

Source: RxJS interop with Angular signals (angular.dev)

debounceTime()

debounced()ExperimentalBack to RxJS
RxJS
query$ = new BehaviorSubject('');

results$ = this.query$.pipe(
  debounceTime(300),
  switchMap((q) => this.http.get<Result[]>('/api/search', { params: { q } })),
);
Signals
query = signal('');
debouncedQuery = debounced(this.query, 300);

results = httpResource<Result[]>(() => ({
  url: '/api/search',
  params: { q: this.debouncedQuery.value() },
}));

debounced() returns a resource that settles 300 ms after the last change. It is experimental in Angular 22, so production code may prefer toObservable() with debounceTime(), shown in the toObservable() mapping.

Source: Debouncing signals with debounced (angular.dev)

@Output() + EventEmitter.emit()

outputFromObservable()Back to RxJS
RxJS
@Output() scrolled = new EventEmitter<number>();

ngOnInit() {
  fromEvent(window, 'scroll')
    .pipe(throttleTime(100), map(() => window.scrollY), takeUntil(this.destroy$))
    .subscribe((y) => this.scrolled.emit(y));
}
Signals
scrolled = outputFromObservable(
  fromEvent(window, 'scroll').pipe(throttleTime(100), map(() => window.scrollY)),
);

The output subscribes to the Observable and forwards every value to parent listeners, and unsubscribes when the component is destroyed. For outputs you emit by hand, use output() instead.

Source: RxJS interop with component outputs (angular.dev)

componentRef.instance.closed.subscribe()

outputToObservable()Back to RxJS
RxJS
open() {
  const ref = this.vcr.createComponent(DialogComponent);
  ref.instance.closed.pipe(take(1)).subscribe((result) => this.handle(result));
}
Signals
open() {
  const ref = this.vcr.createComponent(DialogComponent);
  outputToObservable(ref.instance.closed)
    .pipe(take(1))
    .subscribe((result) => this.handle(result));
}

An output() is not an Observable, so it has no pipe(). Wrap it with outputToObservable() to use RxJS operators; the Observable completes when the component is destroyed.

Source: RxJS interop with component outputs (angular.dev)

From RxJS to Signals

Most Angular components use RxJS for two jobs: holding state that the template shows, and loading data. Angular 22 covers both with signals. signal(), computed() and linkedSignal() replace Subjects and operator chains for state, and httpResource(), rxResource() and resource(), stable since Angular 22, replace the switchMap plus loading-flag pattern for data. This reference pairs each RxJS pattern with its signal equivalent, grouped into 10 topics. The snippets are class members and assume private http = inject(HttpClient); the part after <!-- template --> goes in the component template.

Quick Mapping Table

RxJSSignalsTopic
rxjs / rxjs/operators@angular/core + rxjs-interopSetup
async pipetoSignal()Setup
startWith(value)toSignal(obs$, { initialValue })Reading streams
BehaviorSubject (emits on subscribe)toSignal(obs$, { requireSync: true })Reading streams
subscribe() in ngOnInittoSignal(obs$, { injector })Reading streams
subscription that outlives the componenttoSignal(obs$, { manualCleanup: true })Reading streams
BehaviorSubject storesignal() + asReadonly()State
subject.next(value)signal.set(value)State
scan()signal.update()State
pipe(map())computed()Derived values
combineLatest()computed()Derived values
distinctUntilChanged()computed(fn, { equal })Derived values
shareReplay(1)httpResource() in a serviceDerived values
subscribe() for side effectseffect()Side effects
takeUntil(this.destroy$)takeUntilDestroyed()Side effects
switchMap(() => interval())effect((onCleanup) => …)Side effects
switchMap(() => http.get())httpResource()httpResource
http.get(url, { params, headers })httpResource(() => ({ url, params, headers }))httpResource
filter() before switchMapreturn undefinedhttpResource
map(validate)httpResource(…, { parse })httpResource
http.get(url, { responseType: 'text' })httpResource.text() / .blob() / .arrayBuffer()httpResource
reportProgress + observe: 'events'httpResource.progress()httpResource
observe: 'response'headers() / statusCode()httpResource
http.post().subscribe()HttpClient + resource.reload()httpResource
switchMap to a service ObservablerxResource({ params, stream })rxResource & resource
from(fetch()) / async ngOnInitresource({ params, loader })rxResource & resource
webSocket().subscribe()rxResource({ stream })rxResource & resource
nested switchMap (dependent requests)params: ({ chain }) => …rxResource & resource
loading flag + finalize()isLoading()Loading & errors
catchError()error() + hasValue()Loading & errors
state: 'loading' | 'loaded' | 'error'status()Loading & errors
refresh$ Subject + startWithreload()Loading & errors
startWith([]){ defaultValue }Loading & errors
BehaviorSubject.next() for a local editresource.update()Loading & errors
subscribe() to reset a fieldlinkedSignal()linkedSignal
withLatestFrom() to keep a selectionlinkedSignal({ source, computation })linkedSignal
@Input() setter feeding a SubjecttoObservable()Back to RxJS
debounceTime()debounced()Back to RxJS
@Output() + EventEmitter.emit()outputFromObservable()Back to RxJS
componentRef.instance.closed.subscribe()outputToObservable()Back to RxJS

API Status in Angular 22

Taken from the stability tags in the @angular/core and @angular/common 22.2 type definitions.

APIImport fromStatus
signal() / computed()@angular/coreStable (since v17)
effect()@angular/coreStable (since v20)
linkedSignal()@angular/coreStable (since v20)
toSignal() / toObservable()@angular/core/rxjs-interopStable (since v20)
takeUntilDestroyed()@angular/core/rxjs-interopStable (since v19)
outputFromObservable() / outputToObservable()@angular/core/rxjs-interopStable (since v19)
resource()@angular/coreStable (since v22)
rxResource()@angular/core/rxjs-interopStable (since v22)
httpResource()@angular/common/httpStable (since v22)
debounced()@angular/coreExperimental (since v22)
resourceFromSnapshots()@angular/coreExperimental (since v22)

A Safe Migration Order

  1. Upgrade to Angular 22 so the resource APIs are stable; the 21 → 22 upgrade plan lists every step.
  2. Swap the async pipe for toSignal() at the edges, leaving services that return Observables as they are.
  3. Turn BehaviorSubject stores into signal() and their derived streams into computed().
  4. Replace switchMap data loading with httpResource(), or rxResource() where a service already returns an Observable, and delete the loading and error flags.
  5. Keep RxJS where it earns its place: mutations through HttpClient, WebSockets, and timing operators via toObservable().

Migrating forms as well? See the Reactive Forms to Signal Forms reference.

Frequently Asked Questions

Are httpResource, rxResource and resource stable?
Yes. They were experimental from Angular 19 to 21 and are stable from Angular 22. resource() comes from @angular/core, rxResource() from @angular/core/rxjs-interop and httpResource() from @angular/common/http.
Do I have to remove RxJS?
No. RxJS is still a dependency of Angular and HttpClient still returns Observables. Signals are for state you read in templates and computed values; RxJS remains a good fit for event streams with timing operators. toSignal() and toObservable() connect the two.
What is the difference between httpResource and rxResource?
httpResource() builds the HTTP request for you from a URL or request object and exposes headers, statusCode and progress. rxResource() wraps any Observable you already have, such as a service method that returns this.http.get(), a WebSocket or a store selector.
Should I use httpResource for POST or PUT requests?
No. Resources are for reading data. They cancel the in-flight request when their params change, which could abort a write. Keep mutations on HttpClient and call reload() or update() on the resource afterwards.
When should I use linkedSignal instead of computed?
Use computed() when the value is fully derived and read-only. Use linkedSignal() when the user can also change it, such as a selected item that should reset (or be kept) when the list of options changes.
Why does toSignal throw NG0203?
toSignal(), toObservable(), takeUntilDestroyed() without an argument, and the resource functions need an injection context. Create them in a field initializer or the constructor, or pass an injector option.

Sources & Official Resources

Every signal snippet follows the official Angular guides and was type-checked against Angular 22.2. Last reviewed .

More in Angular tools.