How can I share the startWith(false) state between the 3 streams? I tried using withLatestFrom() but got some weird errors for the value.
const Home = componentFromStream(prop$ => {
const { handler: toggleHandler, stream: toggle$ } = createEventHandler();
const { handler: showHandler, stream: show$ } = createEventHandler();
const { handler: hideHandler, stream: hide$ } = createEventHandler();
const modal$ = merge(
toggle$.pipe(
startWith(false),
map(() => prev => !prev),
scan((state, changeState: any) => changeState(state))
),
show$.pipe(
startWith(false),
map(() => prev => true),
scan((state, changeState: any) => changeState(state))
),
hide$.pipe(
startWith(false),
map(() => prev => false),
scan((state, changeState: any) => changeState(state))
)
);
return combineLatest(prop$, modal$).pipe(
map(([props, modal]) => {
console.log(modal);
return (
<div>
<button onClick={toggleHandler}>Toggle</button>
<button onClick={showHandler}>Show</button>
<button onClick={hideHandler}>Hide</button>
<h1>{modal ? 'Visible' : 'Hidden'}</h1>
</div>
);
})
);
});
In the example, the toggle doesn't respect the current value of show or hide, but only of its own latest value.
https://stackblitz.com/edit/react-rxjs6-recompose
In order to do this, you will need to manage state a bit differently, something similar to what people do in redux. Take a look at example:
const { of, merge, fromEvent } = rxjs; // = require("rxjs")
const { map, scan } = rxjs.operators; // = require("rxjs/operators")
const toggle$ = fromEvent(document.getElementById('toggle'), 'click');
const show$ = fromEvent(document.getElementById('show'), 'click');
const hide$ = fromEvent(document.getElementById('hide'), 'click');
const reduce = (state, change) => change(state);
const initialState = false;
const state$ = merge(
of(e => e),
toggle$.pipe(map(e => state => !state)),
show$.pipe(map(e => state => true)),
hide$.pipe(map(e => state => false)),
).pipe(
scan(reduce, initialState),
);
state$.subscribe(e => console.log('state: ', e));
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
<button id="toggle">Toggle</button>
<button id="show">Show</button>
<button id="hide">Hide</button>
To better understand how it works, take a look at Creating applications article from rxjs documentation
First having startWith on all those observables means they will all emit at the beginning false. Which is like you pushed all those buttons at once.
I think you should try to achieve different behaviour. Using startWith you want to set the initial state of the modal property, right? Therefore it should come after merging those stream together.
To toggle the value you need two things:
place where to store the state (usually accumulator of scan)
differentiate those button presses. You have three buttons therefore you need three values.
This is my approach:
const modal$ = merge(
show$.pipe(mapTo(true)),
hide$.pipe(mapTo(false)),
toggle$.pipe(mapTo(null))
).pipe(
startWith(false),
scan((acc, curr) => {
if (curr === null) {
return !acc;
} else {
return curr;
}
})
);
Show button always emits true, hide button emits false and toggle button emits null. I merge them together and we want to start with false. Next there is scan which holds the state in accumulator.
When null comes it returns negated state. When true or false comes it returns it - that way it sets the new state regardless the previous value.
I took everyone's approach & came up with this. It keeps the toggle() logic inside the map function, & uses startWith() to set the value.
const modal$ = merge(
toggle$.pipe(
map(() => prev => !prev),
),
show$.pipe(
map(() => prev => true),
),
hide$.pipe(
map(() => prev => false),
)
).pipe(
startWith(false),
scan((state, change) => change(state)),
);
Related
I added a start, stop, pause button. Start will start a count down timer which will start from a value, keep decrementing until value reaches 0. We can pause the timer on clicking the pause button. On click of Stop also timer observable completes.
However, once the timer is completed ( either when value reaches 0 or
when clicked on stop button ), I am not able to start properly. I
tried adding repeatWhen operator. It starts on clicking twice. Not at
the first time.
Also, at stop, value is not resetting back to the initial value.
const subscription = merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
tap(val => {
console.log(val);
}),
switchMap(val => (val ? interval(10).pipe(takeUntil(stopClick$)) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
repeatWhen(() => startClick$),
startWith(startValue)
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Stackblitz Code link is available here
This is a pretty complicated usecase. There are two issues I think:
You have two subscriptions to startClick$ and the order of subscriptions matters in this case. When the chain completes repeatWhen is waiting for startClick$ to emit. However, when you click the button the emission is first propagated into the first subscription inside merge(...) and does nothing because the chain has already completed. Only after that it resubscribes thanks to repeatWhen but you have to press the button again to trigger the switchMap() operator.
When you use repeatWhen() it'll resubscribe every time the inner Observable emits so you want it to emit on startClick$ but only once. At the same time you don't want it to complete so you need to use something like this:
repeatWhen(notifier$ => notifier$.pipe(
switchMap(() => startClick$.pipe(take(1))),
)),
So to avoid all that I think you can just complete the chain using takeUntil(stopClick$) and then immediatelly resubscribe with repeat() to start over.
merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
switchMap(val => (val ? interval(10) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$),
repeat(),
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Your updated demo: https://stackblitz.com/edit/rxjs-tum4xq?file=index.ts
Here's an example stopwatch that counts up instead of down. Perhaps you can re-tool it.
type StopwatchAction = "START" | "STOP" | "RESET" | "END";
function createStopwatch(
control$: Observable<StopwatchAction>,
interval = 1000
): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const ticker = timer(0, interval).pipe(
map(x => count++)
);
const end$ = of("END");
return concat(
control$,
end$
).pipe(
catchError(_ => end$),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker;
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker;
}
}
return EMPTY;
})
);
});
}
Here's an example of this in use:
const start$: Observable<StopwatchAction> = fromEvent(startBtn, 'click').pipe(mapTo("START"));
const reset$: Observable<StopwatchAction> = fromEvent(resetBtn, 'click').pipe(mapTo("RESET"));
createStopwatch(merge(start$,reset$)).subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hoursField.innerHTML = Math.floor(seconds / 3600);
});
You can achieve that in another way without completing the main observable or resubscribing to it using takeUntil, repeatWhen, or other operators, like the following:
create a simple state to handle the counter changes (count, isTicking)
merge all the observables that affecting the counter within one observable.
create intermediate observable to interact with the main merge observable (start/stop counting).
interface CounterStateModel {
count: number;
isTicking: boolean;
}
// Setup counter state
const initialCounterState: CounterStateModel = {
count: startValue,
isTicking: false
};
const patchCounterState = new Subject<Partial<CounterStateModel>>();
const counterCommands$ = merge(
startClick$.pipe(mapTo({ isTicking: true })),
pauseBtn$.pipe(mapTo({ isTicking: false })),
stopClick$.pipe(mapTo({ ...initialCounterState })),
patchCounterState.asObservable()
);
const counterState$: Observable<CounterStateModel> = counterCommands$.pipe(
startWith(initialCounterState),
scan(
(counterState: CounterStateModel, command): CounterStateModel => ({
...counterState,
...command
})
),
shareReplay(1)
);
const isTicking$ = counterState$.pipe(
map(state => state.isTicking),
distinctUntilChanged()
);
const commandFromTick$ = isTicking$.pipe(
switchMap(isTicking => (isTicking ? timer(0, 10) : NEVER)),
withLatestFrom(counterState$, (_, counterState) => ({
count: counterState.count
})),
tap(({ count }) => {
if (count) {
patchCounterState.next({ count: count - 1 });
} else {
patchCounterState.next({ ...initialCounterState });
}
})
);
const commandFromReset$ = stopClick$.pipe(mapTo({ ...initialCounterState }));
merge(commandFromTick$, commandFromReset$)
.pipe(startWith(initialCounterState))
.subscribe(
state => (counterDisplayHeader.innerHTML = state.count.toString())
);
Also here is the working version:
https://stackblitz.com/edit/rxjs-o86zg5
Essentially I need:
const saveObservable = new Subject().asObservable();
const create$ = of("ID").pipe(tap(() => console.log("executed")), shareReplay());
const subscription = saveObservable.pipe(
concatMap(({ files = [], ...attributes }) =>
create$.pipe(
tap(id => console.log("queue updates to", id))
)
)
).subscribe();
saveObservable.next({})
This will make it so my initial save operation: of("ID") only executes once. Then, all further executions of this save will use the ID returned and queue up.
What I'm struggling with is that I can't put create$ inside my concatMap because it creates a new instance of the observable and shareReplay is effectively useless.
But I basically need it within the concatMap so that I can use attributes.
How can I do that?
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
const create$ = fromFetch("https://www.google.com", { attributes }).pipe(tap(() => console.log("executed")), shareReplay());
return create$.pipe(
tap(a => console.log(a))
)
})
);
vs.
const create$ = fromFetch("https://www.google.com", { attributes?? }).pipe(tap(() => console.log("executed")), shareReplay());
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
return create$.pipe(
tap(a => console.log(a))
)
})
)
Not sure if this is the best approach, but here's what comes to mind.
You can use a ReplaySubject as a subscriber. When used this way, it will cache the emitted values that come from the source.
So you could have something like this:
const replSubj = new ReplaySubject(/* ... */);
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
// We first subscribe to `replSubj` so we can get the stored values
// If none of the stored ones match the condition imposed in `first`,
// simply emit `of({ key: null })`, which means that a request will be made
return merge(replSubj, of({ key: null }))
.pipe(
// Check if we've had a request with such attributes before
// If yes: just return the stored value received from the subject
// If not: make a request and store the value
// By using `first` we also make sure the subject won't have redundant subscribers
first(v => v.key === attributes.identifier || v.key === null),
switchMap(
v => v.key === null
? fromFetch("https://www.google.com", { attributes }).pipe(tap(() => console.log("executed")))
.pipe(
map(response => ({ key: attributes.identifer, response })), // Add the key so we can distinguish it later
tap(({ response }) => replSubj.next(response)) // Store the value in the subject
)
: of(v.response) // Emit & complete immediately
),
)
}),
);
Note that ReplaySubject can have a second parameter, windowTime, which specifies how long the values should be cached.
I solved it with:
let create$: Observable<EnvelopeSummary>;
const [, , done] = useObservable(
updateObservable$.pipe(
concatMap(attributes => {
if (create$) {
return create$.pipe(
concatMap(({ envelopeId }) =>
updateEnvelope({ ...userInfo, envelopeId, attributes }).pipe(
mapTo(attributes.status)
)
)
)
} else {
create$ = createEnvelope({ ...userInfo, attributes }).pipe(
shareReplay()
);
return create$.pipe(mapTo(attributes.status))
}
}),
takeWhile(status => status !== "sent")
)
);
I have a BehaviorSubject stream of functions. I have an initialState object represented as an immutable Record. Those functions are scanned and used to manipulate the state. The code looks like this:
const initialState = Record({
todo: Record({
title: "",
}),
todos: List([Record({title: "first todo"})()])
})
const actionCreator = (update) => ({
addTodo(title) {
update.next((state) => {
console.log({title}); // for debugging reasons
const todo = Record({title})()
return state.set("todos", state.get("todos").push(todo))
})
},
typeNewTodoTitle(title) {
update.next((state) => state.set("todo", state.get("todo").set("title", title))
})
})
const update$ = new BehaviorSubject(state => state);
const actions = actionCreator(update$);
const state = update$.pipe(
scan(
(state, updater) => updater(state), initialState()
),
// share() without share weird things happen
)
I have a very simple test written for this
it("should only respond to and call actions once", () => {
const subscripition = chai.spy();
const addTodo = chai.spy.on(actions, 'addTodo');
const typeNewTodoTitle = chai.spy.on(actions, 'typeNewTodoTitle');
state
.pipe(
map(s => s.get("todo")),
distinctUntilChanged()
)
.subscribe(subscripition);
state
.pipe(
map(s => s.get("todos")),
distinctUntilChanged()
)
.subscribe(subscripition);
actions.addTodo('test');
expect(subscripition).to.have.been.called.twice // error
actions.typeNewTodoTitle('test');
expect(subscripition).to.have.been.called.exactly(3) // error
expect(addTodo).to.have.been.called.once
expect(typeNewTodoTitle).to.have.been.called.once
});
});
The first strange behavior is that subscription has been called 3 times and then 4 instead of 2 and then 3 times. The second strange behavior is that even though each action has only been called once, the console.log has been called twice. I can fix this problem by adding share() to the pipeline, but I can't figure out why that's required.
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
I have two source observables.
I would like to merge the two source observables, but the merged observable sould complete as soon as one of the source observables completes.
Desired behavior:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4--x
In case of an error on one of the sources, the error should propagate to the merged observable:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------e
"merged" ---1---2----3--4--ex
The "merge" operator only completes the merged stream when both sources have completed:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4-----------------------------x
How can I achieve my desired behavior?
You need to work with the metadata, information about each observable. To do this, use the materialize() operator on each stream and the use dematerialize() on the merged stream to actually emit the data.
Observable.merge( observableA.materialize(),
observableB.materialize() )
.takeWhile( notification -> notification.hasValue() )
.dematerialize()
.subscribe( ... );
This will merge the two observables until either one of them completes or emits an error.
I sure hope someone else answers with more elegant method but this works.
I think you would have to use one of the take operators. You could complete all sources when one source completes like so:
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
Rx.Observable.merge(a.takeUntil(b.last()), b.takeUntil(a.last()))
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Or a less readable but more scaleable version:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
merge(a, b)
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Here is an illustration with error propagation:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
const c = Rx.Observable.timer(2200).map(x => { throw 'oops!'; });
merge(a, b, c)
.subscribe(
x => { console.log('next', x); },
x => { console.log('error', x); },
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Using the takeUntil outside on the merge is tricky as you would loose the last emitted value.
When an an observable completes, it does not emit a value, but we can concat it with another 'signal' observable that emits a single value. We can then watch for the 'signal' observable's value with the takeWhile operator.
Of course you'd have to ensure that the 'signal' observable's emitted value is not a value that could be emitted by the observables that are being merged - an empty object will suffice if the takeWhile predicate compares by reference.
Here's an example:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9);
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Errors will also get propagated:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9)
.concat(Rx.Observable.throw(`the world's about to end`));
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
I ended up rolling my own:
import { Observable } from 'rxjs';
export function whileAll<T>(...observables: Observable<T>[]): Observable<T> {
return new Observable<T>(function (observer) {
if (observables.length === 0)
observer.complete();
else {
const next = observer.next.bind(observer);
const error = observer.error.bind(observer);
const complete = observer.complete.bind(observer);
for (let i = 0; i < observables.length; i++)
observer.add(observables[i].subscribe(next, error, complete));
}
});
}