RxJS: How to use startWith with pairwise

Combine the two operators to emit on the first value

Author's image
Tamás Sallai
2 mins

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]

123pairwise1223

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]

123startWith(0)0123pairwise011223

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

105040pairwise10505040map4010

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

105040startWith0105040pairwise01010505040map104010

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));
25 December 2020