RxJS: How to use startWith with pairwise

Combine the two operators to emit on the first value

Author's image
Tamás Sallai
1 min

pairwise and startWith

The pairwise operator is a great tool if you also need the previous value for every element. It uses a buffer with a size of 2, making it memory efficient as well. The problem is that it does not emit on the first element, making its results one item shorter than the input.

rxjs.of(1, 2, 3).pipe(
  rxjs.operators.pairwise()
).subscribe(console.log.bind(console));

// [1, 2]
// [2, 3]

Depending on the use-case, it might be a good thing. But if you need an element for every input item, combine it with the startWith operator:

rxjs.of(1, 2, 3).pipe(
  rxjs.operators.startWith(0),
  rxjs.operators.pairwise()
).subscribe(console.log.bind(console));

// [0, 1]
// [1, 2]
// [2. 3]

Calculating differences

This helps when you need to calculate differences with a known start point. For example, let's say something starts from the 0 coordinate and the stream consists of points it goes to. Using pairwise in this case offers an easy way to calculate the differences:

rxjs.of(10,50,40).pipe(
	rxjs.operators.pairwise(),
	rxjs.operators.map(([from, to]) => Math.abs(from - to)),
).subscribe(console.log.bind(console));

// 40, 10

In this case, to know how far it moved, you need also to add the starting point of 0 with the startWith operator:

rxjs.of(10,50,40).pipe(
	rxjs.operators.startWith(0),
	rxjs.operators.pairwise(),
	rxjs.operators.map(([from, to]) => Math.abs(from - to)),
).subscribe(console.log.bind(console));

// 10, 40, 10

When not to use startWith with pairwise

Let's say you want to draw circles on mouse clicks and also want to connect them with lines. Since lines need a start and an end coordinate, pairwise is a good operator for them. On the other hand, circles only need the current mouse coordinates.

It's tempting to combine drawing the two shapes in a subscription:

const clicks = rxjs.fromEvent(svg, "click").pipe(
	rxjs.operators.map((e) => ({x: e.offsetX, y: e.offsetY})),
);

// broken: won't draw a circle on the first click
clicks.pipe(
	rxjs.operators.pairwise(),
)
	.subscribe(([p1, p2]) => {
		drawLine(p1, p2);
		drawCircle(p2);
	})

The above implementation does not draw a circle on the first click as pairwise does not emit an element for that. A quick fix is to use pairwise and check the edge case when the first element is undefined:

clicks.pipe(
	rxjs.operators.startWith(undefined),
	rxjs.operators.pairwise(),
)
	.subscribe(([p1, p2]) => {
		if (p1 !== undefined) {
			drawLine(p1, p2);
		}
		drawCircle(p2);
	})

The problem with this is that it meshes two things, one that needs elements and one that needs pairs. A better solution is to separate the two and only use pairwise for the latter:

clicks.pipe(
	rxjs.operators.pairwise(),
)
	.subscribe(([p1, p2]) => {
		drawLine(p1, p2);
	});

clicks.subscribe((p) => drawCircle(p));
December 25, 2020
In this article