devside
devside

Reputation: 2217

withLatestFrom unexpected behavior

I have an unexpected behavior with the operator withLatestFrom.

Output

map a
a
map a <== why a is mapped again ?
map b
b

const { Subject, operators } = window.rxjs
const { map, withLatestFrom } = operators


const createA = new Subject()
const createB = new Subject()

const a = createA.pipe(
  map(() => console.log('map a'))
)

const b = createB.pipe(
  withLatestFrom(a),
  map(() => console.log('map b'))
)

a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })

createA.next()
createB.next()
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/rxjs.umd.js"></script>

Upvotes: 1

Views: 186

Answers (2)

Mrk Sef
Mrk Sef

Reputation: 8022

The problem here isn't with withLatestFrom() but rather with how subscriptions work. Observables are lazy and don't run until you subscribe. Each new subscriptions will run the observable again.

const stream$ = from([1,2,3]);
stream$.subscribe(console.log) // output: 1 2 3
stream$.subscribe(console.log) // output: 1 2 3

In this case, `from([1,2,3)] ran twice. If I alter my stream, anything I do will happen for each subscriber.

const stream$ = from([1,2,3]).pipe(
  tap(_ => console.log("hi"))
);
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3

The final piece of the puzzle is this: internally withLatestFrom() subscribes to the stream that you give it. Just like an explicit .subscribe() runs the observable, so does withLatestFrom() once it's subscribed to.

You can use shareReplay to cache the latest values and replay them instead of running the observable again. It's one way to manage a multicasted stream:

const createA = new Subject()
const createB = new Subject()

const a = createA.pipe(
  tap(() => console.log('tap a')),
  shareReplay(1)
)

const b = createB.pipe(
  withLatestFrom(a),
  tap(() => console.log('tap b'))
)

a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })

createA.next()
createB.next()

Now a.subscribe() and withLatestFrom(a) are both getting a buffered value that only gets run when createA.next() is executed.


As an aside, mapping a value to nothing is bad habit to get into. Consider the following code:

from([1,2,3]).pipe(
  map(val => console.log(val))
).subscribe(val => console.log(val));

This will output

1
undefined
2
undefined
3
undefined

because you're actually mapping each value to nothing. tap on the other hand doesn't change the source observable, so it's a much better tool for debugging and/or side effects that don't alter the stream

from([1,2,3]).pipe(
  tap(val => console.log(val))
).subscribe(val => console.log(val));

This will output

1
1
2
2
3
3

Upvotes: 2

devside
devside

Reputation: 2217

I found that the operator share() allows multiple subscribers.

const a = createA.pipe(
  map(() => console.log('map a')),
  share()
)

Upvotes: 1

Related Questions