I am new to RxJs. Can someone help me understand why withLatestFrom is freezing when I wrap it inside a switchMap ?
withLatestFromNoCancel() {
const hi$ = of("hi");
const click$ = fromEvent(document.getElementById("click1"), "click");
const example = click$.pipe(withLatestFrom(click$, hi$));
const subscribe = example.subscribe(val => console.log(val));
}
withLatestFromStrangeWithSwitchMap() {
const hi$ = of("hi");
const click$ = fromEvent(document.getElementById("click1"), "click");
const example = click$.pipe(
switchMap(e => of(e).pipe(withLatestFrom(click$, hi$))),
);
const subscribe = example.subscribe(val => console.log(val));
}
withLatestFrom emits only when its source Observable emits. In your case it's of(e) that emits just once and then completes so it never reacts to click$, hi$.
So it's not because of switchMap. That's how withLatestFrom works.
Related
I want to implement a progress bar with rxjs,so I need to get the position of mousedown and the position of mousemove after mousedown.here is the test code I write.
useEffect(() => {
const start$ = fromEvent(divRef.current, 'mousedown').pipe(
tap(() => console.log('start')),
map(event => [event.clientX, event.clientY])
)
const move$ = fromEvent(divRef.current, 'mousemove').pipe(
tap(() => console.log('move')),
map(event => [event.clientX, event.clientY])
)
const end$ = fromEvent(divRef.current, 'mouseup')
const drag$ = concat(start$, move$).pipe(takeUntil(end$), repeat())
const subscription = drag$.subscribe(([newX, newY]) => {
setX(newX)
setY(newY)
})
return () => {
subscription.unsubscribe()
}
})
what I think is that when I click down the mouse,I will subscribe the Start$,and then if I move,I will subscribe the move$.But the Phenomenon is quiet different from what I thought.The log in the console just output 'start'.
enter image description here
as you can see from the picture,when I click down and move,I just can subscribe the start$,if I use the concat method wrong.Hope someone can do me a favor.
I assume you want the mouse movements in the order they happen. Here is a small snippet:
import './style.css';
import {
tap,also
fromEvent,
takeUntil,
exhaustMap,
} from 'rxjs';
const mouseDown$ = fromEvent(document, 'mousedown');
const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');
mouseDown$
.pipe(
tap(console.log),
exhaustMap((start) =>
mouseMove$.pipe(tap(console.log), takeUntil(mouseUp$))
)
)
.subscribe();
Please have a look here: Stackblitz
I have three observables foo$, bar$ and baz$ that I merge together to form another observable. This is working as expected:
The stream starts with >>>
Each value are emitted one by one
The stream ends with <<<
const foo$ = of('foo');
const bar$ = of('bar');
const baz$ = of('baz');
merge(foo$, bar$, baz$).pipe(startWith('>>>'), endWith('<<<')).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith} = rxjs.operators;</script>
Now if none of the three observables above emit a value, I do not want to output neither >>> nor <<<. So startWith and endWith can only "run" if merge(foo$, bar$, baz$) actually emits a value.
In order to simulate that I'm rejecting all values emitted by foo$, bar$ and baz$ with a filtering function.
const foo$ = of('foo').pipe(filter(() => false));
const bar$ = of('bar').pipe(filter(() => false));
const baz$ = of('baz').pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(startWith('>>>'), endWith('<<<')).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith, filter} = rxjs.operators</script>
However as you can see in the output, both startWith and endWith have emitted their value even though the merge() hasn't produced any.
Question: How can I prevent startWith and endWith from executing if the observable did not emit a single value?
The first condition is simple. You can just prepend the first emission with concatMap:
mergeMap((v, index) => index === 0 ? of('>>>', v) : of(v))
The second condition is more tricky. You want basically the right opposite to defaultIfEmpty. I can't think of any simple solution so I'd probably use endWith anyway and just ignore the emission if it's the first and only emission (which means the source just completed without emitting anything):
endWith('<<<'),
filter((v, index) => index !== 0 || v !== '<<<'),
Complete example:
const foo$ = of('foo');//.pipe(filter(() => false));
const bar$ = of('bar');//).pipe(filter(() => false));
const baz$ = of('baz');//.pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(
mergeMap((v, index) => index === 0 ? of('>>>', v) : of(v)),
endWith('<<<'),
filter((v, index) => index !== 0 || v !== '<<<'),
).subscribe(console.log);
Live demo: https://stackblitz.com/edit/rxjs-n2alkc?file=index.ts
You could assign the merged observable to a variable, take the first element from the stream and then map to the observable you want to execute when atleast one value has been emitted.
const foo$ = of('foo').pipe(filter(() => false))
const bar$ = of('bar').pipe(filter(() => false))
const baz$ = of('baz').pipe(filter(() => false))
const merged$ = merge(foo$, bar$, baz$);
merged$.pipe(
take(1),
switchMap(() => merged$.pipe(
startWith('>>>'),
endWith('<<<')
))
).subscribe(console.log);
https://stackblitz.com/edit/rxjs-phspsc
From the documentation of startWith:
Returns an Observable that emits the items you specify as arguments before it begins to emit items emitted by the source Observable.
So startWith and endWith will always run.
I dont know what your expected result should be, but if you only want to concatenate the strings for each emitted value you could use the map or switchMap operators.
EDIT:
Example with map to concat each value:
const foo$ = of('foo').pipe(filter(() => false));
const bar$ = of('bar').pipe(filter(() => false));
const baz$ = of('baz').pipe(filter(() => false));
merge(foo$, bar$, baz$).pipe(map(v => `>>>${v}<<<`)).subscribe(str => {
console.log(str);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of} = rxjs; const {startWith, endWith, filter, map} = rxjs.operators</script>
I see that all answers are done combining operators, but doing this solution with only operators is kind of tricky, why not just create your own observable? I think this solution is the easiest one to understand. No hidden cleverness, no complex operator combinations, no subscription repetitions...
Solution:
const foo$ = of('foo')//.pipe(filter(() => false));
const bar$ = of('bar')//.pipe(filter(() => false));
const baz$ = of('baz')//.pipe(filter(() => false));
function wrapIfOnEmit$(...obs) {
return new Observable(observer => {
let hasEmitted;
const subscription = merge(...obs).subscribe((data) => {
if (!hasEmitted) {
observer.next('>>>');
hasEmitted = true;
}
observer.next(data);
},
(error) => observer.error(error),
() => {
if (hasEmitted) observer.next('<<<');
observer.complete();
})
return () => subscription.unsubscribe();
})
}
wrapIfOnEmit$(foo$, bar$, baz$).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
<script>const {merge, of, Observable} = rxjs; const {filter} = rxjs.operators</script>
Hope this helps!
One way to achieve that is to use a materialize-dematerialize operators pair:
source$.pipe(
// turn all events on stream into Notifications
materialize(),
// wrap elements only if they are present
switchMap((event, index) => {
// if its first event and its a value
if (index === 0 && event.kind === 'N') {
const startingNotification = new Notification('N', '>>>', undefined);
return of(startingNotification, event);
}
// if its a completion event and it not a first event
if (index > 0 && event.kind === 'C') {
const endingNotification = new Notification('N', '<<<', undefined);
return of(endingNotification, event);
}
return of(event);
}),
// turn Notifications back to events on stream
dematerialize()
)
Play with this code in a playground:
https://thinkrx.io/gist/5a7a7f1338737e452ff6a1937b5fe05a
for convenience, I've added an empty and error source there as well
Hope this helps
Here here the requirement:
When click start button, emit event x times every 100ms, each emit correspond an UI update. When x times emit complete, it will trigger a final UI update, look simple right?
Here is my code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
.pipe(
take(x),
share()
)
var startLight$ = start$
.pipe(
switchMap(() => {
intervel$
.pipe(last())
.subscribe(() => {
// Update UI
})
return intervel$
}),
share()
)
startLight$
.subscribe(function (e) {
//Update UI
})
Obviously, subscribe inside switchMap is anti-pattern, so I tried to refactor my code:
const startInterval$ = start$
.pipe(
switchMapTo(intervel$),
)
startInterval$.pipe(last())
.subscribe(() => {
//NEVER Receive value
})
const startLight$ = startInterval$.pipe(share())
The problem is that intervel$ stream is generated inside switchMap and can not be accessed outside, you can only access the stream who generate interval$, i.e. start$ which never complete!
Is there is smarter way to handle such kind of problem or it was an inherent limitation of rxjs?
You were very close. Use last() inside intervel$ to only emit the final one to the subscribe below. Working StackBlitz. Here are details from the StackBlitz:
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100)
.pipe(
tap(() => console.log('update UI')), // Update UI here
take(x),
last()
);
const startInterval$ = start$
.pipe( switchMapTo(intervel$));
startInterval$
.subscribe(() => {
console.log('will run once');
});
Update
If you do not wish to use tap(), then you can simply cause start$ to finish by taking only the first emission and then completing with either take(1) or first(). Here is a new StackBlitz showing this.
const start$ = fromEvent(document.getElementById('start'), 'click')
.pipe(
first()
);
const intervel$ = interval(100)
.pipe(
take(x)
);
const startInterval$ = start$
.pipe(
switchMapTo(intervel$)
);
startInterval$
.subscribe(
() => console.log('Update UI'),
err => console.log('Error ', err),
() => console.log('Run once at the end')
);
The downside to this approach (or any approach that completes the Observable) is that once completed it won't be reused. So for example, clicking multiple times on the button in the new StackBlitz won't work. Which approach to use (the first one that can be clicked over and over or the one that completes) depends on the results you need.
Yet Another Option
Create two intervel$ observables, one for the intermediate UI updates and one for the final one. Merge them together and only do the UI updating in the subscribe. StackBlitz for this option
code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel1$ = interval(100)
.pipe(
take(x)
);
const intervel2$ = interval(100)
.pipe(
take(x+1),
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
A more idiomatic way, same logic as previous one (By Guichi)
import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
import { fromEvent, interval, merge } from 'rxjs';
const x = 5;
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100);
const intervel1$ = intervel$
.pipe(
take(x)
);
const intervel2$ = intervel1$
.pipe(
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
Reflection
The key problem of the original question is to 'use the same observable in different ways', i.e. during the progress and the final. So merge is an pretty decent logic pattern to target this kind of problem
Put your update logic inside the switchMap and tap() , tap will run multiple time and only last emission will be taken by subscribe()
const startInterval$ = start$
.pipe(
switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
)
startInterval$
.subscribe(() => {
// will run one time
})
a: 1---2-3-4--5---6
b: ------T---------
o: ------1234-5---6
Using RxJS, is there some operator that can accomplish the diagram above? I have stream A which is a random stream of events, given a stream B which has a single true event, can I have an output stream that doesn't emit anything until that true event, and then sends everything is had saved up until then and afterwards emits normally?
I thought maybe I could use buffer(), but it seems like there is no way to do a one time buffer like this with that operator.
const { concat, interval, of, from } = rxjs;
const { share, delay, toArray, takeUntil, mergeMap } = rxjs.operators;
const waitUntil = signal$ => source$ => {
const sharedSource$ = source$.pipe(share());
return concat(
sharedSource$.pipe(
takeUntil(signal$),
toArray(),
mergeMap(from)
),
sharedSource$
);
}
const stopWaiting$ = of('signal').pipe(delay(2000));
const source$ = interval(500).pipe(
waitUntil(stopWaiting$)
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>
I think #ZahiC's solution is correct but personally I'd do it in a single chain using the multicast operator.
a$.pipe(
multicast(new Subject(), s => concat(
s.pipe(
buffer(b$),
take(1),
),
s
)),
)
multicast will basically spit the stream into two where concat will first subscribe to the first one that is buffered until b$ emits. Then it completes immediately because of take(1) and concat subscribe to the same steam again but this time unbuffered.
Here's my solution, using TypeScript:
export const queueUntil = <T>(signal$: Observable<any>) => (source$: Observable<T>) => {
let shouldBuffer = true;
return source$.pipe(
bufferWhen(() => shouldBuffer ? signal$.pipe(
tap(() => shouldBuffer = false),
) : source$),
concatMap(v => v),
);
};
and can be used like this:
a$.pipe(
queueUntil(b$)
)
All existing answers here (as of 2022/1/4) have the potential to skip/eat source notifications emitted on the same frame as the notifier emits (particularly if the source stream feeds new values back into itself). This solution supports this use case:
function waitUntil<T>(notifier$: Observable<any>): MonoTypeOperatorFunction<T> {
return (source$: Observable<T>) => {
const buffer$ = new ReplaySubject<T>();
let doBuffer = true;
source$.pipe(takeWhile(() => doBuffer, true)).subscribe(buffer$);
return notifier$.pipe(
take(1),
switchMap(() => {
doBuffer = false;
return concat(buffer$, source$);
}),
);
};
}
I'm listening to mousemove event until mouseup. I'm doing it with takeUntil.
My code:
const onMouseMove = fromEvent(window, "mousemove");
const onMouseUp = fromEvent(window, "mouseup");
const resizePanel = onMouseMove
.pipe(
takeUntil(onMouseUp),
map(
(event: MouseEvent) => {
this.isDragging = true;
this.resizePanel(event.clientX);
}
)
);
I have one variable isDragging: boolean, which I want to set to false, when mouseup occurs, e.g. after takeUntil in my code. It must be simple, but I can't figure it out.
You may want to try something like this
const onMouseMove = fromEvent(window, "mousemove");
const onMouseUp = fromEvent(window, "mouseup");
const resizePanel = onMouseMove
.pipe(
takeUntil(onMouseUp.pipe(tap(() => this.isDragging = false))),
map(
(event: MouseEvent) => {
this.isDragging = true;
this.resizePanel(event.clientX);
}
)
);
The idea is to add a pipe with tap operator to onMouseUp used as parameter for takeUntil
You could use combineLatest on onMouseMove$ and onMouseUp$ and use a finalise subject in takeUntil.
const { fromEvent, combineLatest, Subject } = rxjs;
const { takeUntil } = rxjs.operators;
const onMouseMove$ = fromEvent(window, "mousemove");
const onMouseUp$ = fromEvent(document, "mouseup");
const finalise = new Subject();
combineLatest(onMouseMove$, onMouseUp$).pipe(takeUntil(finalise)).subscribe(([onMouseMove, onMouseUp]) => {
if (onMouseUp) {
console.log('mouse up');
finalise.next(true);
finalise.complete();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.min.js"></script>
You need to subscribe
const { fromEvent } = rxjs;
const onMouseUp$ = fromEvent(document, "mouseup");
onMouseUp$.subscribe(onMouseUp => {
console.log('mouse up');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.min.js"></script>
takeUntil ends an observable, nothing happens after takeUntil.