Skip to content

combineLatestWith - Combine Latest Values Within a Pipeline

The combineLatestWith operator combines the latest values of the original Observable and other specified Observables into a new stream. This is the Pipeable Operator version of the Creation Function combineLatest.

🔰 Basic Syntax and Usage

ts
import { interval } from 'rxjs';
import { combineLatestWith, map } from 'rxjs';

const source1$ = interval(1000); // 0, 1, 2, ...
const source2$ = interval(1500); // 0, 1, 2, ...

source1$
  .pipe(
    combineLatestWith(source2$),
    map(([val1, val2]) => `Stream1: ${val1}, Stream2: ${val2}`)
  )
  .subscribe(console.log);

// Example output:
// Stream1: 0, Stream2: 0
// Stream1: 1, Stream2: 0
// Stream1: 2, Stream2: 0
// Stream1: 2, Stream2: 1
// Stream1: 3, Stream2: 1
// ...
  • Wait until all streams have issued at least once, then output the latest value combination every time one of them issues.
  • Because it is accepted in tuple form, it is type-safe in TypeScript.

🌐 RxJS Official Documentation - combineLatestWith

💡 Typical Usage Patterns

  • Real-time form validation: Combine and validate the latest values of multiple fields
  • Multiple sensor integration: Simultaneous display of different frequencies of data such as temperature, humidity, etc.
  • Combined search filters: Integrate category selection and keyword entry
  • Live preview: Real-time preview combining multiple configuration values

🧠 Practical Code Example (with UI)

Example of real-time color (RGB) change with multiple sliders.

ts
import { fromEvent, combineLatest } from 'rxjs';
import { map, startWith, combineLatestWith } from 'rxjs';

// Build the UI
const container = document.createElement('div');
container.innerHTML = `
  <h3>combineLatestWith Practical Example: RGB Color Picker</h3>
  <div>
    <label>Red: <input type="range" id="red" min="0" max="255" value="128"></label>
    <span id="red-value">128</span>
  </div>
  <div>
    <label>Green: <input type="range" id="green" min="0" max="255" value="128"></label>
    <span id="green-value">128</span>
  </div>
  <div>
    <label>Blue: <input type="range" id="blue" min="0" max="255" value="128"></label>
    <span id="blue-value">128</span>
  </div>
  <div id="preview" style="width: 200px; height: 100px; border: 1px solid #ccc; margin-top: 10px;"></div>
`;
document.body.appendChild(container);

// Get slider elements
const redSlider = document.getElementById('red') as HTMLInputElement;
const greenSlider = document.getElementById('green') as HTMLInputElement;
const blueSlider = document.getElementById('blue') as HTMLInputElement;

const redValue = document.getElementById('red-value')!;
const greenValue = document.getElementById('green-value')!;
const blueValue = document.getElementById('blue-value')!;
const preview = document.getElementById('preview')!;

// Stream for each slider
const red$ = fromEvent(redSlider, 'input').pipe(
  map(e => Number((e.target as HTMLInputElement).value)),
  startWith(128)
);

const green$ = fromEvent(greenSlider, 'input').pipe(
  map(e => Number((e.target as HTMLInputElement).value)),
  startWith(128)
);

const blue$ = fromEvent(blueSlider, 'input').pipe(
  map(e => Number((e.target as HTMLInputElement).value)),
  startWith(128)
);

// ✅ Pipeable Operator version - integrate others into main stream
red$
  .pipe(
    combineLatestWith(green$, blue$)
  )
  .subscribe(([r, g, b]) => {
    // Update value display
    redValue.textContent = String(r);
    greenValue.textContent = String(g);
    blueValue.textContent = String(b);

    // Update preview background color
    preview.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
  });
  • Moving any slider will immediately update the preview with the latest RGB values combined.
  • After all sliders have been manipulated at least once, the latest combination is always reflected.

🔄 Difference from Creation Function combineLatest

Basic Differences

combineLatest (Creation Function)combineLatestWith (Pipeable Operator)
Usage LocationUsed as independent functionUsed within .pipe() chain
SyntaxcombineLatest([obs1$, obs2$, obs3$])obs1$.pipe(combineLatestWith(obs2$, obs3$))
First StreamTreats all equallyTreats as main stream
AdvantageSimple and readableEasy to combine with other operators

Specific Usage Examples

Creation Function is Recommended for Simple Combination Only

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

const width$ = fromEvent(window, 'resize').pipe(map(() => window.innerWidth));
const height$ = fromEvent(window, 'resize').pipe(map(() => window.innerHeight));

// Simple and readable
combineLatest([width$, height$]).subscribe(([w, h]) => {
  console.log(`Window size: ${w} x ${h}`);
});

Pipeable Operator is Recommended When Adding Transformation Processing to Main Stream

ts
import { fromEvent, interval } from 'rxjs';
import { combineLatestWith, map, startWith, throttleTime } from 'rxjs';

const clicks$ = fromEvent(document, 'click');
const timer$ = interval(1000);

// ✅ Pipeable Operator version - completed in one pipeline
clicks$
  .pipe(
    throttleTime(500),           // Prevent rapid clicking
    map(() => Date.now()),       // Convert to timestamp
    startWith(0),                // Set initial value
    combineLatestWith(timer$),   // Integrate with timer
    map(([clickTime, tick]) => ({
      lastClick: clickTime,
      elapsed: tick
    }))
  )
  .subscribe(data => {
    console.log(`Last click: ${data.lastClick}, Elapsed: ${data.elapsed} seconds`);
  });

// ❌ Creation Function version - becomes verbose
import { combineLatest } from 'rxjs';
combineLatest([
  clicks$.pipe(
    throttleTime(500),
    map(() => Date.now()),
    startWith(0)
  ),
  timer$
]).pipe(
  map(([clickTime, tick]) => ({
    lastClick: clickTime,
    elapsed: tick
  }))
).subscribe(data => {
  console.log(`Last click: ${data.lastClick}, Elapsed: ${data.elapsed} seconds`);
});

Summary

  • combineLatest: Optimal for simply combining multiple streams
  • combineLatestWith: Optimal when you want to integrate other streams while transforming or processing the main stream

⚠️ Important Notes

Wait Until All Streams Emit at Least Once

Values will not be output until all Observables have emitted at least once.

ts
import { of, timer } from 'rxjs';
import { combineLatestWith } from 'rxjs';

of(1, 2, 3).pipe(
  combineLatestWith(
    timer(1000),  // Emits after 1 second
  )
).subscribe(console.log);
// Output: [3, 0]
// * Waits until timer$ emits, then combines with the latest value (3) of of() at that time

Beware of High Frequency Updates

If either stream is updated frequently, the combined result will be output frequently accordingly.

ts
import { interval } from 'rxjs';
import { combineLatestWith, take } from 'rxjs';

interval(100).pipe(
  take(5),
  combineLatestWith(interval(1000).pipe(take(3)))
).subscribe(console.log);
// Output:
// [0, 0]
// [1, 0]
// [2, 0]
// [3, 0]
// [4, 0]
// [4, 1]
// [4, 2]

Control update frequency with throttleTime or debounceTime as needed.

ts
import { fromEvent, interval } from 'rxjs';
import { combineLatestWith, throttleTime, map } from 'rxjs';

const mouseMoves$ = fromEvent(document, 'mousemove').pipe(
  throttleTime(100),  // Limit every 100ms
  map(e => ({ x: (e as MouseEvent).clientX, y: (e as MouseEvent).clientY }))
);

const timer$ = interval(1000);

mouseMoves$
  .pipe(combineLatestWith(timer$))
  .subscribe(([pos, tick]) => {
    console.log(`Position: (${pos.x}, ${pos.y}), Tick: ${tick}`);
  });

Error Handling

If an error occurs in any Observable, the entire stream terminates with an error.

ts
import { throwError, interval } from 'rxjs';
import { combineLatestWith, take, catchError } from 'rxjs';
import { of } from 'rxjs';

interval(1000).pipe(
  take(2),
  combineLatestWith(
    throwError(() => new Error('Error occurred')).pipe(
      catchError(err => of('Error recovered'))
    )
  )
).subscribe({
  next: console.log,
  error: err => console.error('Error:', err.message)
});
// Output: [1, 'Error recovered']

Released under the CC-BY-4.0 license.