Action on subscribe() and unsubscribe() - javascript

in RxJS I'd like to take some special action once an Observer subscribes to an Observable and when he unsubscribes. I can of course overwrite the subscribe() and unsubscribe() methods, but that seems crud.
Isn't there a way during creation of the observable to supply callbacks that get called whenever someone subscribes/unsubscribes?
BR,
Daniel

This is what Observable.create is for. You can create your own observable with specified attach/detach handlers, and can even wrap existing observables with just 2 additional lines of code.
const obs = Rx.Observable.create(observer => {
console.log('attach');
// If you want to wrap another observable, call this:
// const subs = other.subscribe(observer);
return () => {
// subs.unsubscribe();
console.log('detach');
};
});
console.log('subscribe subscription1');
const subscribtion1 = obs.subscribe(() => {});
console.log('subscribe subscription2');
const subscribtion2 = obs.subscribe(() => {});
setTimeout(() => {
console.log('subscribtion1.dispose()');
subscribtion1.unsubscribe();
}, 500);
setTimeout(() => {
console.log('subscribtion2.dispose()');
subscribtion2.unsubscribe();
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.0-rc.4/Rx.js"></script>

Related

Redux: Dispatch actions in sequence

I'm creating a Reddit client on Redux and I have 2 store dispatches firing in the app:
// App()
const dispatch = useDispatch();
useEffect(() => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
dispatch(fetchUser());
dispatch(fetchSubs());
}
});
...
However, I want fetchUser() to run and finish before fetchSubs() can begin, as the former currently seems to ruin API calls for the latter while it's running. How can I solve this?
Since you are using createAsyncThunk you can do something like this:
dispatch(fetchUser())
.unwrap()
.then((user) => {
// do anything you want with user, or don't, also dispatch actions
dispatch(fetchSubs());
})
.catch((e) => {
// error in case of rejection inside createAsyncThunk second argument
console.log(e);
});
Explanation
Let's say const thunk = fetchUser()
so basically dispatch(fetchUser()) is the same as dispatch(thunk).
Redux's dispatch function returns whatever its argument (the action) returns.
So in this case, dispatch(thunk) returns whatever thunk returns.
thunk, based on how createAsyncThunk works, returns a promise that either resolves to fulfilled action, or the rejected action. (those actions that you receive in extra reducers).
This is how you can access those actions:
dispatch(thunk).then(fullfilledAction=>...).catch(rejectedAction=>...`
RTK library also provides a method called unwrap. Instead of those action objects I explained above, it lets you use the returned value from the 2nd argument of createAsyncThunk.
export const fetchUser = createAsyncThunk("user/fetchUser", async () => {
const user = await Reddit.getUser().then(val => {
return val;
});
return user; // unwrap lets you use this instead of action objects.
})
Try this with pure react and redux hooks
...
const state = useStore(yourStore) //use your method to read state
const dispatch = useDispatch();
const checkValue = () => {
const stateMatch = window.location.href.match(/state=([^&]*)/);
const codeMatch = window.location.href.match(/code=([^&]*)/);
if ((stateMatch && codeMatch) || localStorage.getItem("access_token")) {
return true;
}
return false;
}
useEffect(() => {
if(checkValue())
dispatch(fetchUser());
}
}, []);
useEffect(() => {
if(checkValue() && state.authState)
dispatch(fetchSubs());
}
}, [state.authState]);
...

How to return outer observable and not inner in a High-order observables

Lets clarify the problem with the following code:
this.rates$ = this._glbRateService.getRates(params); // 1
this.rates$.pipe(
mergeMap(rates => {
const priceByRates: Observable<any>[] = rates.map(rate => {
const paramsRatingItemProduct = {
idItem: product.idItem,
idRate: rate.idRate
};
return this._glbRatingItemProduct.getPrice(paramsRatingItemProduct); // 2
});
return priceByRates;
})
).subscribe(response => {
console.log(response); // 3
});
In that code:
I get rates from server
For every rate, I get prices (map)
My console.log returns the value from the inner subscription (this._glbRatingItemProduct.getPr...)
And what I want is to do logic with the mapping values and the inner subscription.
Something like this:
this.rates$ = this._glbRateService.getRates(params);
this.rates$.pipe(
mergeMap(rates => {
const priceByRates: Observable<any>[] = rates.map(rate => {
const paramsRatingItemProduct = {
idItem: product.idItem,
idRate: rate.idRate
};
return this._glbRatingItemProduct.getPrice(paramsRatingItemProduct);
// WITH THE SUBSCRIPTION OF THIS RETURN I WANT TO MAKE LOGIC
// WITH rates.map, and then return rates, NOT THE INNER SUBSCRIPTION
});
return priceByRates;
})
).subscribe(response => {
console.log(response);
});
You first need to execute the inner observable array first with maybe forkJoin
then run your mapping function with the array
mergeMap(rates => {
const priceByRates: Observable<any>[] = rates.map(rate => {
const paramsRatingItemProduct = {
idItem: product.idItem,
idRate: rate.idRate
};
return this._glbRatingItemProduct.getPrice(paramsRatingItemProduct);
});
return forkJoin(...priceByRates).pipe((values)=>values.map....your logic ));
})
https://www.learnrxjs.io/learn-rxjs/operators/combination/forkjoin
It's sometimes helpful to separate out the logic of mapping and flattening higher-order observables. Here it should be a bit clearer that map() returns an array of observables and forkJoin() joins all those observables into one stream.
this.rates$ = this._glbRateService.getRates(params);
this.rates$.pipe(
map(rates => rates.map(
rate => this._glbRatingItemProduct.getPrice({
idItem: product.idItem,
idRate: rate.idRate
})
),
mergeMap(priceByRates => forkJoin(priceByRates))
).subscribe(console.log);
On the other hand, forkJoin() only emits once all source observables complete. If you don't need all the responses together, you keep your source streams de-coupled with a simpler merge(). Only one line needs to change:
mergeMap(priceByRates => merge(...priceByRates))
The thing to remember is that mergeMap expects a single stream to be returned. It will convert an array into a stream of values. So mergeMap(num => [10,9,8,7,num]) doesn't map num into an array, it creates a new stream that will emit those numbers one at a time.
That's why mergeMap(_ => val : Observable[]) will just emit each observable, (as a higher order observable) one at a time.
With this knowledge, you can actually change your stream to merge without using the static merge function above. That could look like this:
this.rates$ = this._glbRateService.getRates(params);
this.rates$.pipe(
mergeMap(rates => rates.map(
rate => this._glbRatingItemProduct.getPrice({
idItem: product.idItem,
idRate: rate.idRate
})
),
mergeAll()
).subscribe(console.log);
mergeAll() will take each higher-order observable as it arrives and subscribe+merge their output.

Async/Await inside the Observable

How can I use async/await inside the Observable??
With this code I'm unable to trigger the unsubscribe function within observable thus interval is not cleared.
const { Observable } = require("rxjs");
const test = () => new Observable(async (subscriber) => {
await Promise.resolve();
const a = setInterval(() => {
subscriber.next(Math.random());
console.log("zz");
}, 500);
return () => {
console.log("asdsad");
clearInterval(a);
};
});
const xyz = test().subscribe(console.log);
setTimeout(() => {
xyz.unsubscribe();
}, 3000);
Async/Await inside an observable is not supported. However, it can be done with a behavior subject and an asynchronous nested function.
Create a behavior subject, convert it to an observable (.asObservable()), execute the asynchronous nested function, return the observable. Here's an example.
function getProgress() {
// Change this value with latest details
const value = new BehaviorSubject('10%');
const observable = value.asObservable();
// Create an async function
const observer = async() => {
// Perform all tasks in here
const wait1 = await new Promise(resolve => setTimeout(resolve, 3000));
value.next('66%');
const wait2 = await new Promise(resolve => setTimeout(resolve, 3000));
value.next('100%');
// Complete observable
value.complete();
}
// Call async function & return observable
observer();
return observable;
}
It's very readable and works like a charm.
First of all, subscriber passed to observable contructor cannot be async function. There is no support for that.
If you need to create observable from promise, use from:
import { from } from 'rxjs';
const observable = from(promise);
But considering your scenario.
Because there is no way to cancel native js promise, you cannot realy unsubscribe from such created observable, so:
const obs = from(new Promise(resolve => {
setTimeout(() => {
console.log('gonna resolve');
resolve('foo');
}, 1000);
}));
const sub = obs.subscribe(console.log);
setTimeout(() => sub.unsubscribe(), 500);
will print:
gonna resolve
gonna resolve
gonna resolve
(...)
so yeah: gonna resolve will be printed in the cosole all the time, but nothing more - result passed to resolve will be ignored - just not logged.
From the other hand, if you remove that unsubscribtion (setTimeout(() => sub.unsubscribe(), 500);) this time you will see:
gonna resolve
foo
gonna resolve
gonna resolve
gonna resolve
(...)
There is one way that maybe will help you - defer - but it's not strictly related with your question.
import { defer } from 'rxjs';
defer(async () => {
const a = await Promise.resolve(1);
const b = a + await Promise.resolve(2);
return a + b + await Promise.resolve(3);
}).subscribe(x => console.log(x)) // logs 7

rxjs Observable: handle unsubscribe of all subscribe

I have a method that return a Observable
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
now i subscribe it three time, and after 5 second unsubscribe all:
const sub1 = subFoo(1).subscribe(result => console.log(result));
const sub2 = subFoo(2).subscribe(result => console.log(result));
const sub3 = subFoo(3).subscribe(result => console.log(result));
setTimeout(() => {
sub1.unsubscribe();
sub2.unsubscribe();
sub3.unsubscribe();
}, 5000);
i can handle the complete unsubscrite of all listners?
eg. (in pseudo code):
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
// something like this
observer.onAllListenerAreUnsubscribed(() => {
console.log('All Listener Are Unsubscribed!');
});
setTimeout(() => {
observer.next(id);
}, 1000);
});
}
Live demo: https://stackblitz.com/edit/angular-ayl12r
EDIT July 2022: The same functionality can be achieved since RxJS 7.0 with tap({ subscribe: () => ... }).
An Observable can't know about subscriptions to its chains. If you want to be able to tell how many times someone subscribed you can count it yourself:
let subscriptions = 0;
subFoo(id): Observable<number> {
return new Observable<number>(observer => {
subscriptions++;
...
return (() => {
if (--subscriptions === 0) {
// do whatever...
}
...
})
})
})
You can also collect all subscriptions on the "observer side" into a single Subscription and then add a custom handler when you unsubscribe:
const subs = new Subscription();
subs.add(subFoo(1).subscribe(...));
subs.add(subFoo(2).subscribe(...));
subs.add(subFoo(3).subscribe(...));
subs.add(() => {
// do whatever...
});
subs.unsubscribe(); // Will unsubscribe all subscriptions and then call your custom method.
you can unsubscribe all listeners in one single line so no need to handle that event
subscriptions.add(sub1).add(sub2).add(sub3);
// Then unsubscribe all of them with a single
subscriptions.unsubscribe();
By completing all your observables at once, you are sure you will not get any data leakage.
You can create a subject that will emit once the observables should stop emitting and use the takeUntil() operator on your observables, like so:
const completeSubscription: Subject<void> = new Subject();
const sub1 = subFoo(1)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub2 = subFoo(2)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
const sub3 = subFoo(3)
.pipe(takeUntil(completeSubscription))
.subscribe(result => console.log(result));
setTimeout(() => {
completeSubscription.next();
completeSubscription.complete();
}, 5000);

Chaining Observables in RxJS

I'm learning RxJS and Angular 2. Let's say I have a promise chain with multiple async function calls which depend on the previous one's result which looks like:
var promiseChain = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
}).then((result) => {
console.log(result);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result + 2);
}, 1000);
});
}).then((result) => {
console.log(result);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result + 3);
}, 1000);
});
});
promiseChain.then((finalResult) => {
console.log(finalResult);
});
My attempts at doing the same solely using RxJS without the use of promises produced the following:
var observableChain = Observable.create((observer) => {
setTimeout(() => {
observer.next(1);
observer.complete();
}, 1000);
}).flatMap((result) => {
console.log(result);
return Observable.create((observer) => {
setTimeout(() => {
observer.next(result + 2);
observer.complete()
}, 1000);
});
}).flatMap((result) => {
console.log(result);
return Observable.create((observer) => {
setTimeout(() => {
observer.next(result + 3);
observer.complete()
}, 1000);
});
});
observableChain.subscribe((finalResult) => {
console.log(finalResult);
});
It yields the same output as the promise chain. My questions are
Am I doing this right? Are there any RxJS related improvements that I can make to the above code
How do I get this observable chain to execute repeatedly? i.e. Adding another subscription at the end just produces an additional 6 though I expect it to print 1, 3 and 6.
observableChain.subscribe((finalResult) => {
console.log(finalResult);
});
observableChain.subscribe((finalResult) => {
console.log(finalResult);
});
1
3
6
6
About promise composition vs. Rxjs, as this is a frequently asked question, you can refer to a number of previously asked questions on SO, among which :
How to do the chain sequence in rxjs
RxJS Promise Composition (passing data)
RxJS sequence equvalent to promise.then()?
Basically, flatMap is the equivalent of Promise.then.
For your second question, do you want to replay values already emitted, or do you want to process new values as they arrive? In the first case, check the publishReplay operator. In the second case, standard subscription is enough. However you might need to be aware of the cold. vs. hot dichotomy depending on your source (cf. Hot and Cold observables : are there 'hot' and 'cold' operators? for an illustrated explanation of the concept)
Example for clarification:
Top of pipe can emit n values (this answers "How do I get this observable chain to execute repeatedly"), but subsequent chained streams emit one value (hence mimicing promises).
// Emit three values into the top of this pipe
const topOfPipe = of<string>('chaining', 'some', 'observables');
// If any of the chained observables emit more than 1 value
// then don't use this unless you understand what is going to happen.
const firstObservablePipe = of(1);
const secondObservablePipe = of(2);
const thirdObservablePipe = of(3);
const fourthObservablePipe = of(4);
const addToPreviousStream = (previous) => map(current => previous + current);
const first = (one) => firstObservablePipe.pipe(addToPreviousStream(one));
const second = (two) => secondObservablePipe.pipe(addToPreviousStream(two));
const third = (three) => thirdObservablePipe.pipe(addToPreviousStream(three));
const fourth = (four) => fourthObservablePipe.pipe(addToPreviousStream(four));
topOfPipe.pipe(
mergeMap(first),
mergeMap(second),
mergeMap(third),
mergeMap(fourth),
).subscribe(console.log);
// Output: chaining1234 some1234 observables1234
You could also use concatMap or switchMap. They all have subtle differences. See rxjs docs to understand.
mergeMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/mergemap
concatMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/concatmap
switchMap:
https://www.learnrxjs.io/learn-rxjs/operators/transformation/switchmap

Categories