skip to content

In RxJS 7, why were the pipeable merge, concat, zip, race and combineLatest operators deprecated, and what replaces them?

level: middleimportance: nice to knowfreq 30%

answer

  1. same name, two functions
  2. one import site: 'rxjs'
  3. the With suffix
  4. which joins have only one form

basics

~20 s

Each shared its name with a static creation function, which clashed once all operators moved into the 'rxjs' entry point; they were renamed mergeWith, concatWith, zipWith, raceWith and combineLatestWith, and the old names are deprecated.

solid answer

~40 s

In RxJS 6 there were two functions called `merge`: the static creation function from `'rxjs'` and a pipeable operator from `'rxjs/operators'`, and likewise for `concat`, `zip`, `race` and `combineLatest`. RxJS 7 renamed the operator forms with a `With` suffix — `mergeWith`, `concatWith`, `zipWith`, `raceWith`, `combineLatestWith` — and since 7.2 the operators are exported from `'rxjs'` itself, where the static name was already taken. The old pipeable forms remain only in `'rxjs/operators'`, deprecated and slated for removal in v8. Semantics are unchanged: `a$.pipe(mergeWith(b$))` behaves like `merge(a$, b$)`, with the piped source placed first. `forkJoin` has only a static form and `withLatestFrom` only an operator form.

code

ts · 12 lines
ts
import { Observable, combineLatestWith, filter, map } from 'rxjs';

declare const query$: Observable<string>;
declare const sort$: Observable<'asc' | 'desc'>;

// One main pipeline, a second input joined midway
const request$ = query$.pipe(
  map((q) => q.trim()),
  filter((q) => q.length > 2),
  combineLatestWith(sort$),
  map(([q, sort]) => ({ q, sort })),
);

go deeper

for a junior

Recall that in RxJS 7 the pipeable joins are named mergeWith, concatWith, zipWith, raceWith and combineLatestWith, imported from 'rxjs'.

for a middle

Explain the naming clash, the 7.2 move of operators into 'rxjs', that the With operators equal the static function with the piped source first, and which joins have only one form.

for a senior

Plan an RxJS 6-to-7 cleanup: find deprecated operator imports, rename them without behaviour changes, and replace rest-argument and scheduler-argument calls before v8 removes them.

for a principal

Weigh when to run such a cleanup — lint rules and codemods across many teams versus leaving deprecated but working code — against the cost of a forced migration later.

## Two functions with one name RxJS offers most joins in two shapes: - a **static creation function**, which takes all sources as arguments and returns a new observable: `merge(a$, b$)`; - a **pipeable operator**, which is used inside `pipe` and treats the piped observable as the first source: `a$.pipe(mergeWith(b$))`. In RxJS 6 both shapes had the **same name**. `merge` imported from `'rxjs'` was the static function; `merge` imported from `'rxjs/operators'` was the operator. The same went for `concat`, `zip`, `race` and `combineLatest`. Which one you got depended entirely on the import line, which made code harder to read and review. ## What RxJS 7 changed 1. During the RxJS 7 development cycle the operator forms were **renamed** with a `With` suffix. The changelog describes them as renames of the legacy operators, not new behaviour. 2. RxJS **7.2** began exporting the operators from `'rxjs'` itself and recommends importing from there. One module cannot export two functions under one name, so the static names kept the short names and the operators kept the `With` names. 3. The old pipeable forms still exist in `'rxjs/operators'`, marked `@deprecated` with "Replaced with …With. Will be removed in v8." | Deprecated pipeable operator | Static function | Current pipeable operator | |---|---|---| | `merge` | `merge` | `mergeWith` | | `concat` | `concat` | `concatWith` | | `zip` | `zip` | `zipWith` | | `race` | `race` | `raceWith` | | `combineLatest` | `combineLatest` | `combineLatestWith` | ## Same semantics, source first The renamed operators behave exactly like the static function with the piped observable inserted as the **first** source: - `a$.pipe(mergeWith(b$))` is `merge(a$, b$)` — interleaved by arrival. - `a$.pipe(concatWith(b$))` is `concat(a$, b$)` — `b$` subscribed after `a$` completes. - `a$.pipe(zipWith(b$))` is `zip([a$, b$])` — pairs by position. - `a$.pipe(combineLatestWith(b$))` is `combineLatest([a$, b$])` — arrays of latest values. - `a$.pipe(raceWith(b$))` is `race([a$, b$])` — the first source to emit wins. `race` deserves a note because it is asked less often: it subscribes to all sources, and when one emits its first value the others are **unsubscribed** and the winner is mirrored from then on, including its completion and error. An error from any source before a winner emerges errors the result. ```ts // RxJS 6 style: deprecated in RxJS 7, slated for removal in v8 import { merge } from 'rxjs/operators'; const old$ = a$.pipe(merge(b$)); // RxJS 7.2+ style import { merge, mergeWith } from 'rxjs'; const s$ = merge(a$, b$); // static creation function const p$ = a$.pipe(mergeWith(b$)); // pipeable operator, same values ``` ## Joins with only one shape - **`forkJoin`** exists only as a static function. There is no pipeable form, because "wait for this stream and those others to complete" is naturally written as `forkJoin([a$, b$])`. - **`withLatestFrom`** exists only as an operator, because it is inherently source-driven: the piped observable is the trigger and the arguments are companions. ## Choosing the form in code - Use the **static function** when the sources are peers and none is "the" stream — a dashboard of several inputs reads best as `combineLatest({ … })`. - Use the **`With` operator** when one stream is the main pipeline and you are adding others to it midway, after some mapping or filtering. - Import both from `'rxjs'`. Code that still imports `merge` or `combineLatest` from `'rxjs/operators'` is using the deprecated operator; the fix is a rename plus an import change, with no behaviour change. Other deprecations from the same era sit on the static functions themselves: passing sources to `combineLatest` and `forkJoin` as separate arguments instead of an array or object, and passing a scheduler argument, are both deprecated and slated for removal in v8.

  • In RxJS, what does race([a$, b$]) do with the losing sources?
    It subscribes to all of them, and as soon as one emits its first value it unsubscribes from the others and mirrors the winner from then on, including its completion or error. An error from any source before a first value arrives errors the whole result. `a$.pipe(raceWith(b$))` is the pipeable form.
  • Why is there no forkJoinWith and no static withLatestFrom?
    `forkJoin` treats all sources as equal peers that must each complete, so the static array or object form says everything. `withLatestFrom` is inherently asymmetric — the piped observable triggers and the arguments are passive companions — so an operator is its only sensible shape.

saying these in an interview costs you the question

  • mergeWith behaves differently from the static merge and needs different handling
  • The pipeable combineLatest from rxjs/operators is the current recommended form
  • The deprecated operators have already been removed from RxJS 7.8
  • forkJoin can be used as a pipeable operator inside pipe()
  • withLatestFrom is deprecated in favour of a withLatestFromWith operator