I have been trying to use rxjs properly with Redux observable and Redux actions. I've tried many different combinations of calling the below functions and every possible combination has given me different errors.
Basically, I have two actions that can be dispatched in my app: SET_CIRCULAR_DATA and SET_MODAL_OVER_DEEP_LINK_FLAG.
I'd like another action to be dispatched, determineFoundInProducts() when SET_MODAL_OVER_DEEP_LINK_FLAG happens followed by the first (successful) SET_CIRCULAR_DATA action. Any subsequent SET_CIRCULAR_DATA actions should be ignored until SET_MODAL_OVER_DEEP_LINK_FLAG happens again. Because of this need, I felt withLatestFrom() made the most sense. I also thought that takeUntil() and repeat() might be helpful to help me accomplish what I need, though I'm not including them in this code example because I'm stuck at the withLatestFrom() part.
I've tried different combinations of piping, not piping, using switchMap, map, mapTo, but nothing seems to work. I am importing all the used rxjs functions and I'm combining epics at the root, so it couldn't be that. I just can't seem to get the right combination to not get this to error.
Some examples of errors are
Actions must be plain objects. Use custom middleware for async actions.
You provided 'undefined' where a stream was expected
I am using RXJS ^5.5.6 and Redux Observable ^0.17.0.
import 'rxjs';
import { Observable } from 'rxjs/Observable';
import { withLatestFrom, map, tap, mergeMap } from 'rxjs/operators';
import ....all the types and actions
export const handleProductDetailsModalOnLoginRedirect = (action) =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA).pipe(
withLatestFrom(
action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG)
),
mergeMap(([first, second]) => {
console.log(first, second);
return determineFoundInProducts();
})
);
EDIT
I was struggling in my original post to determine what part of the code was breaking. What I had trouble with was that the redux observable action could not communicate with the rxjs functions. What I found was that piping appropriately piped (pun intended) the action to be workable with rxjs functions. Here is what I did.
action.ofType(navigationTypes.types.INIT_REDIRECT_MODAL_LOGIC)
.switchMap(() =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA)
.withLatestFrom(action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG))
.take(1)
.pipe(
map(([first]) => {
// Do stuff
return determineFoundInProducts();
}))
)
The answer depends on what you want to happen if multiple SET_MODAL_OVER_DEEP_LINK_FLAG actions are dispatched before any SET_CIRCULAR_DATA action is, as well as whether they should some how be paired together with some sort of unique ID or not.
It sounds most likely like you're looking for exhaustMap.
The exhaustMap operator is basically the opposite behavior of switchMap. It maps inputs to inner observable and flattens its output, ignore other input values until that observable completes. It exhausts the inner Observable.
In your use case it means that we first listen for SET_MODAL_OVER_DEEP_LINK_FLAG, then we listen for a single SET_CIRCULAR_DATA, but while we're waiting for that subsequent SET_CIRCULAR_DATA we ignore all possibly incoming SET_MODAL_OVER_DEEP_LINK_FLAG actions. Note that the take(1) is important, because otherwise we'll listen for a stream of every SET_CIRCULAR_DATA, not just one.
export const handleProductDetailsModalOnLoginRedirect = (action) =>
action.ofType(navigationTypes.types.SET_MODAL_OVER_DEEP_LINK_FLAG)
.exhaustMap(() =>
action.ofType(weeklyAdTypes.types.SET_CIRCULAR_DATA)
.take(1)
.map(() => determineFoundInProducts())
);
Btw it's common to ask Redux Observable questions that are actually just RxJS questions. This is a cool thing to note because the RxJS community is massive so you'll find a TON more resources on it, as well as have a better chance of getting help if you're able to rephrase questions to be agnostic of Redux Observable--e.g. change ofType('TYPE') to a filter(d => d.type === 'TYPE')
The above answer will definitely help others. However, what I was struggling with was having redux observable action communicate with RXJS functions. Please see my edit to my original post for solution.
Related
Imagine the following simple case:
you get some data from a source, resulting in an observable, lets call it obs$
you need to transform it (frex a simple synchronous sort) and hand the result some kind of subject, lets call it subject$$
You now have several ways to implement it. The simplest might look like:
obs$
.pipe(
takeUntil(this.#destroy$$)
)
.subscribe(
result => {
this.subject$$.next(result.dSomething())
});
Works like a charm. A bit later you write some more similar functions, and thus would like to separate the transformation from the assignment. Basically you move the doSomething from the subscribe to a map in the pipe. This could look like:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(res => { this.subject$$.next(res) });
Works fine as well. Though the assignment now looks a bit clunky, surely there is a better way to do this? You might be tempted to go for something like:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(this.subject$$.next);
But quickly find that this fails, as you've got no access to your outer scopes this. The suggested solution looks like this:
obs$
.pipe(
takeUntil(this.#destroy$$),
map(result => result.dSomething())
)
.subscribe(this.subject$$);
Which works just fine in many cases.
Imagine my surprise, when all of a sudden I stumbled into a waterfall of errors:
EmptyError: no elements in sequence
showing up in four completely unrelated places, bubbling up through the code, without an obvious place to even try catch it.
Now following the white rabbit, I searched for far to long until I finally managed to discover the place of origin. Still it doesn't make any sense to me.
Why is the observer "disconnecting" for a moment when there is no activity that could result in a complete? Funnily enough, the subject is catching back up shortly afterwards, so this is not completely breaking the assignment.
Of course, I can always use the slightly more verbose assignment, but understanding why this fails only in some cases would really help avoid those.
Its very hard to suggest anything because I don't know what part of your code and what RxJS chain gives you the EmptyError.
However, I'm suspicious that the problem comes from .subscribe(this.subject$$) because this is not the same as .subscribe(res => this.subject$$.next(res));
All Subjects behave like Observables and observers at the same time. Observers implement this interface that handles all three notification types. If you use .subscribe(this.subject$$) you're passing also error and complete notifications from the source Observable to subject$$ and this is very like your problem. When obs$ completes, the complete notification is passed to subject$$ that completes as well and since it's a Subject it will never ever emit any next again and to all its new observers it'll just pass complete notification and that's it. So this might explain why you're seeing EmptyError somewhere else in you chains that use subject$$.
With .subscribe(res => this.subject$$.next(res)); you're only passing next notifications and complete or error notifications are ignored.
The Ngrx documentation for using an effect with state selected from the store
states (no pun intended)
Note: For performance reasons, use a flattening operator like concatMap to prevent the
selector from firing until the correct action is dispatched.
...and provides the example:
addBookToCollectionSuccess$ = createEffect(() => this.actions$.pipe(
ofType(CollectionApiActions.addBookSuccess),
concatMap(action => of(action).pipe(
withLatestFrom(this.store.select(fromBooks.getCollectionBookIds))
)),
tap(([action, bookCollection]) => console.log('whatever'))
), { dispatch: false }
);
The example has since been changed to use concatLatestFrom, but that's not important here. What I'm interested in is understanding the original suggestion. I think I understand the intent, to avoid having withLatestFrom processing results from getCollectionBookIds apart from when the source action triggers, but I don't understand why the solution needs to involve wrapping withLatestFrom in a manually constructed Observable using of(action). Wouldn't something like this be just as effective and a lot more readable?
concatMap(() => this.store.select(selectSelectedBookIds).pipe(first())),
My understanding of Rxjs is not that good and after a few days of trying to working this out I suspect I'm still missing something.
Your suggestions should work fine, if you just need the selected state:
concatMap(() => this.store.select(selectSelectedBookIds).pipe(first())),
However, should you also need the action itself, the following will pass along an array containing both the action and your selected state:
concatMap(action => of(action).pipe(
withLatestFrom(this.store.select(fromBooks.getCollectionBookIds))
)),
I'm going to phrase the question in rxjs, but I suppose it's similar for any Rx or observable library.
Say I have an observable of users, and a function getAssociates(user) that returns another observable of users. I want to use the getAssociates function on every user in the observable and return an observable of those associates. flatMap is enough for this.
But I also want to run getAssociates on each associate that comes back, but without ever running it twice on any given user (since two users might share an associate, and if A has B as an associate, then B also has A as an associate).
Something like the expand operator is what I think I'm looking for:
seedUsers.pipe(
expand(user => getAssociates(user)),
);
but how can I get in the bit about not running twice on any given user? I could maintain a list of seen users, but I'd like to achieve it in a functional style.
Conceptually, you need to :
Keep track of the known users, for example using a Set
Filter the known users before making a request, for example using filter operator
Here is a suggestion :
let knownIds = new Set();
getAllItems(Ids){
return from(Ids).pipe(
filter(id => ! knownIds.has(id)),
concatMap(id => getAllItems(id)),
map( id => knownIds.add(id))
)
}
getAllItems([originalId]).subscribe( allItems=> console.log)
Notes:
I guess you could manage to do it using rxjs, but none of the solutions I can think of is simpler than using a set + filter.
I used concatMap to ensure you don't run the request twice. Using mergeMap (flatMap), you could have a scenario like this :
---------Req(user1)----------------------------resp(user1)-------------------------
---Req(user2)--------resp(user2)--Req(user1)-----------------resp(user1)-
But if you accept having eventually more than 2 requests per user, you can use flatMap to gain speed.
I am having trouble creating a reproducible example so bear with me.
I have a library I created that wraps async API responses in an Observable. Simplifying greatly, the library has functions like lib.ask("end-point") that return an Observable with the response of the API call to that end point.
I want to run that API call every time another event happens. I'm attempting to model that with some version of mergeMap/switchMap/concatMap, depending on the situation. Let's use mergeMap as an example.
If I use map + mergeAll, it works just fine. I am writing it like so:
var responses$ = event$.pipe(
map(()=>lib.ask("end-point")),
mergeAll()
);
When I subscribe to responses$, I get my response!
However, if I rewrite the same chain with mergeMap instead, it does not work:
var responses$ = event$.pipe(
mergeMap(()=>lib.ask("end-point"))
);
In this case, when I subscribe I do not get any results. It almost appears as if the inner subscription never gets made.
Like I said, I am struggling to create a reproducible example. I have attempted to debug what RxJS does to try to find where this thing is failing, but have not been able to figure it out. If anyone can think of why or in what situations mergeMap would behave differently than map + mergeAll, that might help me figure out where my problem is.
I need to create a subscription to an Observable that is immediately disposed of when it is first called.
Is there something like:
observable.subscribeOnce(func);
My use case, I am creating a subscription in an express route handler and the subscription is being called multiple times per request.
Not 100% certain about what you need, but if you only want to observe the first value, then use either first() or take(1):
observable.first().subscribe(func);
note: .take(1) and .first() both unsubscribe automatically when their condition is met
Update from RxJS 5.5+
From comment by Coderer.
import { first } from 'rxjs/operators'
observable
.pipe(first())
.subscribe(func);
Here's why
RxJS has some of the best documentation I've ever come across. Following the bellow link will take you to an exceedingly helpful table mapping use cases to operators. For instance, under the "I want to take the first value" use case are three operators: first, firstOrDefault, and sample.
Note, if an observable sequence completes with no notifications, then the first operator notifies subscribers with an error while the firstOrDefault operator provides a default value to subscribers.
operator use case lookup
UPDATE(DEC/2021):
As toPromise() function has been deprecated in RxJS 7, new functions have been announced to be used instead of it. firstValueFrom and lastValueFrom.
firsValueFrom function resolves the first emitted value and directly unsubscribe from the resource. It rejects with an EmptyError when the Observable completes without emitting any value.
On the other hand, lastValueFrom function is, to a certain degree, same to toPromise() as it resolves the last value emitted when the observable completes. However, if the observable doesn't emit any value, it will reject with an EmptyError. Unlike toPromise() which resolve undefined when no value emits.
For more information, please check the docs.
Old Answer:
If you want to call an Observable only one time, it means you are not going to wait for a stream from it. So using toPromise() instead of subscribe() would be enough in your case as toPromise() doesn't need unsubscription.
To supplement #Brandon's answer, using first() or the like is also essential for updating a BehaviorSubject based on its Observable. For example (untested):
var subject = new BehaviorSubject({1:'apple',2:'banana'});
var observable = subject.asObservable();
observable
.pipe(
first(), // <-- Ensures no stack overflow
flatMap(function(obj) {
obj[3] = 'pear';
return of(obj);
})
)
.subscribe(function(obj) {
subject.next(obj);
});
Clean and Convenient Version
Expanding on M Fuat NUROĞLU's amazing answer on converting the observable to a promise, here's the very convenient version of it.
const value = await observable.toPromise();
console.log(value)
The beauty of this is that we can use that value like a normal variable without introducing another nested block!
This is especially handy when you need to get multiple values from multiple observables. Neat and clean.
const content = await contentObservable.toPromise();
const isAuthenticated = await isAuthenticatedObservable.toPromise();
if(isAuthenticated){
service.foo(content)
}
Of course, you will have to make your containing function async if you are to go with this route. You can also just .then the promise if you don't want the containing function to be async
I'm not sure if there are tradeoffs with this approach, feel free to let me know in the comments so we are aware.
P.S. If you liked this answer, don't forget to upvote M Fuat NUROĞLU's Answer as well :)
I had similar question.
Below was called even later on from different state changers. As I did not want to.
function foo() {
// this was called many times which was not needed
observable.subscribe(func);
changeObservableState("new value");
}
I decided to try unsubscribe() after subscribe as below.
function foo() {
// this was called ONE TIME
observable.subscribe(func).unsubscribe();
changeObservableState("new value");
}
subscribe(func).unsubscribe(); is like subscribeOnce(func).
I hope that helped you too.
observable.pipe(take(1)).subscribe() use take 1 it subscribe for one time then exit