Tutorials Logic, IN info@tutorialslogic.com
RxJS

RxJS Guide: Observables, Operators, Angular and Signals

A practical RxJS guide focused on the most important operators, with beginner-friendly explanations, real use cases, Angular patterns, examples, traps, and practice questions.

RxJS 7.8.x Operators Observables Angular Examples Practice Questions

Quick Summary

  • RxJS is: a library for working with values over time using Observables.
  • Operator means: a function that takes an Observable and returns a new Observable.
  • Use RxJS when: values arrive over time, can be cancelled, combined, filtered, retried, shared, or transformed.
  • Common examples: search input, button clicks, route changes, HTTP calls, WebSockets, timers, forms, and app state.
  • Biggest skill: choosing the right operator for the user experience.
Mental model: source Observable -> operators inside pipe() -> subscriber or async pipe.
Version and migration note: examples were reviewed with the maintained RxJS 7.8.x line in July 2026. For forward-compatible code, replace deprecated pluck with map plus optional property access, prefer configurable retry over retryWhen for ordinary backoff, and replace publish with share or connectable.

Operator Categories

Operator Categories reference table
CategoryImportant operatorsPurpose
Creationof, from, fromEvent, interval, timer, defer, throwErrorCreate Observable sources.
Transformationmap, scan, reduce, bufferTime, pairwiseChange emitted values; use map(value => value?.property) for property selection.
Filteringfilter, take, first, last, skip, distinctUntilChanged, debounceTime, throttleTimeControl which values pass through.
CombinationcombineLatest, withLatestFrom, forkJoin, zip, merge, concat, raceWork with multiple streams.
FlatteningswitchMap, mergeMap, concatMap, exhaustMapMap a value to another Observable and flatten the result.
Error and retrycatchError, retry({ count, delay }), finalizeRecover, retry with a limit and delay, and clean up.
Multicastingshare, shareReplay, connectableShare subscriptions, replay selected values, or control connection timing.

Creation Operators

Creation Operators reference table
OperatorExplanationExample use
ofCreates an Observable from fixed values.Return fallback data such as of([]).
fromCreates an Observable from array, promise, iterable, or similar source.Convert a Promise or array into a stream.
fromEventCreates a stream from DOM events.Listen to clicks, input, scroll, or keyup.
intervalEmits numbers repeatedly after a fixed interval.Polling, timer ticks, live counters.
timerEmits after a delay, optionally repeatedly.Delayed action or scheduled polling.
deferCreates a fresh Observable factory for each subscription.Run current-time logic only when subscribed.
throwErrorCreates an Observable that errors.Return an error from inside an operator.
Creation examples
import { of, from, fromEvent, interval, timer, defer } from 'rxjs';

of('Angular', 'React', 'Vue');
from(fetch('/api/users'));
fromEvent(document, 'click');
interval(1000);
timer(2000);
defer(() => of(new Date().toISOString()));

Transformation Operators

Transformation Operators reference table
OperatorExplanationExample use
mapTransforms each emitted value.Convert API response into view model.
map(value => value?.profile?.name)Selects a nested property with optional chaining.Modern replacement for deprecated pluck('profile', 'name').
scanAccumulates state over time and emits each step.Counter, reducer-style state, running totals.
reduceAccumulates all values and emits once on completion.Final sum after finite stream completes.
pairwiseEmits previous and current value together.Compare route, scroll, or form changes.
bufferTimeCollects values for a time window.Batch frequent events.
toArrayCollects all values into an array when complete.Collect results from finite stream.
Transform user response
users$ = this.http.get<UserDto[]>('/api/users').pipe(
  map(users => users.map(user => ({
    id: user.id,
    label: `${user.firstName} ${user.lastName}`,
    active: user.status === 'ACTIVE'
  })))
);

Filtering and Rate-Control Operators

Filtering and Rate-Control Operators reference table
OperatorExplanationUse when
filterPasses values matching a condition.Ignore invalid form values.
takeTakes a fixed number of values, then completes.Read first 1 or first N values.
takeUntilCompletes when another Observable emits.Cleanup on destroy or cancellation signal.
firstEmits the first matching value.Need first valid event.
skipIgnores the first N values.Ignore initial default state.
distinctUntilChangedSkips same consecutive value.Do not repeat search for same term.
debounceTimeWaits for quiet time before emitting.Search box after typing stops.
throttleTimeEmits at most once per time window.Button click or scroll rate limiting.
auditTimeEmits latest value after each time window.UI updates during scroll/resize.
sampleTimeSamples latest value at intervals.Periodic snapshot of frequent events.
Search input stream

Combination Operators

Combination Operators reference table
OperatorExplanationBest use
combineLatestCombines latest values from multiple streams after each has emitted.View model from filters, sort, page, and data.
withLatestFromMain stream emits while sampling latest values from others.Button click uses latest form state.
forkJoinWaits for all inner Observables to complete, then emits final values.Parallel HTTP calls needed once.
zipPairs values by emission order.Step-by-step pair matching.
mergeRuns streams together and emits as values arrive.Combine multiple event sources.
concatRuns streams one after another.Sequence tasks in order.
raceUses the stream that emits first.Timeout or fastest source wins.
Build a view model
viewModel$ = combineLatest([
  users$,
  searchTerm$,
  selectedRole$
]).pipe(
  map(([users, term, role]) => ({
    users: users.filter(user =>
      user.name.includes(term) && (!role || user.role === role)
    ),
    term,
    role
  }))
);

Flattening Operators

Flattening Operators reference table
OperatorBehaviorBest useAvoid when
switchMapCancels previous inner Observable and switches to latest.Search, route param HTTP calls, latest result wins.Every request must finish.
mergeMapRuns inner Observables concurrently.Independent writes, parallel requests, event fan-out.Order matters or concurrency must be limited.
concatMapQueues inner Observables and runs one at a time.Ordered saves, sequential jobs, queue behavior.Latest request should cancel old one.
exhaustMapIgnores new source values while inner Observable is active.Login, payment, submit button duplicate prevention.User should be able to cancel and start a newer request.
Submit button with exhaustMap
saveClicks$ = fromEvent(saveButton, 'click').pipe(
  exhaustMap(() => this.api.saveForm(this.form.value).pipe(
    catchError(error => {
      this.toast.error('Save failed');
      return EMPTY;
    })
  ))
);

Utility and Side-Effect Operators

Utility and Side-Effect Operators reference table
OperatorExplanationExample use
tapRuns side effects without changing value.Logging, analytics, debugging.
delayDelays emissions.Demo loading, backoff sketch.
timeoutErrors if source takes too long.Fail slow request with fallback.
startWithEmits an initial value before source values.Initial loading or default UI state.
endWithEmits value after source completes.Completion marker.
defaultIfEmptyEmits fallback if source completes without value.No result fallback.
isEmptyEmits whether source had no values.Empty-state detection.

Error Handling Operators

Error Handling Operators reference table
OperatorExplanationImportant rule
catchErrorCatches an error and returns a replacement Observable.Always return an Observable.
retry({ count, delay })Retries a failed source with an explicit limit and fixed or calculated delay.Retry only idempotent operations; use a delay callback when status-aware backoff is required.
finalizeRuns cleanup on complete, error, or unsubscribe.Good for loading flags.
HTTP error handling
users$ = this.http.get<User[]>('/api/users').pipe(
  retry({ count: 2, delay: 500 }),
  catchError(error => {
    this.logger.error(error);
    return of([]);
  }),
  finalize(() => this.loading.set(false))
);

Multicasting and Caching

Multicasting and Caching reference table
ToolExplanationUse carefully
SubjectManual multicast source.Can become global mutable state.
BehaviorSubjectStores latest value and emits it to new subscribers.Good for current state.
ReplaySubjectReplays previous values.Always bound buffer size/time if possible.
shareShares one subscription while subscribers exist.Good for avoiding duplicated side effects.
shareReplayShares subscription and replays cached values.Use correct reset/refCount behavior to avoid stale cache.
connectableCreates a shared Observable whose connection can be started explicitly.Use when connection timing must be controlled; prefer share for the common ref-counted case.
Cache one HTTP result
profile$ = this.http.get<Profile>('/api/profile').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

Angular Operator Patterns

  • Search field: map -> debounceTime -> distinctUntilChanged -> switchMap.
  • Route data: paramMap -> map -> switchMap.
  • Save button: click stream -> exhaustMap.
  • Ordered autosave: form value stream -> concatMap.
  • Dashboard view model: combineLatest of data, filters, and selected state.
  • Manual subscription cleanup: use takeUntilDestroyed from @angular/core/rxjs-interop; pass a DestroyRef when the call is outside an injection context.
  • Observable-to-Signal bridge: create toSignal once in a stable field or service, provide an appropriate initial value, and avoid recreating it during template evaluation.
  • Template rendering: prefer async pipe instead of manual subscribe when possible.
Angular lifecycle and Signal interop
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';

export class ProfileComponent {
  private readonly destroyRef = inject(DestroyRef);

  readonly profile$ = this.api.getProfile();
  readonly profile = toSignal(this.profile$, { initialValue: null });

  constructor() {
    this.audit.events$.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(event => this.record(event));
  }
}

Common Operator Mistakes

  • Using mergeMap for search, causing older responses to overwrite newer results.
  • Using switchMap for writes that must all complete.
  • Putting catchError at the end when only an inner request should recover.
  • Forgetting to return an Observable from catchError.
  • Using unbounded ReplaySubject and retaining too much memory.
  • Creating duplicate HTTP requests because a cold Observable is subscribed multiple times.
  • Hiding business logic in tap instead of keeping it explicit.
  • Manually subscribing in Angular when the async pipe would handle lifecycle.

25 Most Important Practice Questions

1. Which operator transforms each value?
Answer: map.
2. Which operator filters values by condition?
Answer: filter.
3. Which operator waits for typing to stop?
Answer: debounceTime.
4. Which operator skips repeated consecutive values?
Answer: distinctUntilChanged.
5. Which operator cancels old HTTP requests in search?
Answer: switchMap.
6. Which operator runs inner streams concurrently?
Answer: mergeMap.
7. Which operator queues async work in order?
Answer: concatMap.
8. Which operator ignores duplicate submits while one request is active?
Answer: exhaustMap.
9. Which operator combines latest values from multiple streams?
Answer: combineLatest.
10. Which operator waits for multiple HTTP requests to complete once?
Answer: forkJoin.
11. Which operator samples latest state when a button is clicked?
Answer: withLatestFrom.
12. Which operator runs side effects without changing values?
Answer: tap.
13. Which operator catches errors?
Answer: catchError.
14. What must catchError return?
Answer: An Observable, such as of([]) for a fallback or throwError(() => new Error('Request failed')) to rethrow.
15. Which operator runs cleanup on complete, error, or unsubscribe?
Answer: finalize.
16. Which operator retries failed source?
Answer: retry.
17. How should a retry limit and delay be configured?
Answer: Use retry({ count, delay }), and retry only operations that are safe to repeat.
18. Which operator emits an initial value?
Answer: startWith.
19. Which operator accumulates state over time?
Answer: scan.
20. Which operator compares previous and current value?
Answer: pairwise.
21. Which operator batches values by time?
Answer: bufferTime.
22. Which operator shares one subscription?
Answer: share.
23. Which operator shares and replays cached values?
Answer: shareReplay.
24. Which creation operator listens to DOM events?
Answer: fromEvent.
25. Which creation operator emits values every interval?
Answer: interval.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.