I know that we can subscribe to actions and depending on what action is dispatched we can dispatch new actions - it can be safely called the Effect, but ...
What if I want to dispatch some actions after the date I keep in state? I care about the good organization of the code in the project, so it is important for me to properly name such a function.
Can the function subcribe for the passage of time and perform actions if time === 0 can be called an Effect?
mySelector
.getTime(this.store)
.pipe(skipWhile(time => time > 0))
.subscribe(data => {
// this.store.dispatch([...]);
});
you can you store in the effects and it can help to solve your issue.
#Injectable()
export class AuthActivationSelfEffects {
public readonly scheduler$: Observable<Action> = createEffect(() =>
this.actions$.pipe(
switchMap(() => this.store.select(getTime)),
skipWhile(time => time > 0),
switchMap(() => [
action1,
action2,
action3,
]),
),
);
constructor(protected readonly store: Store) {}
}
The right way would be to have a start action and an end action which are triggered somewhere from controllers.
Because effects are for side effects, not for scheduling.
it would look like:
.pipe(
ofType(startAction),
delay(5000), // time to wait
takeUntil(this.actions$.pipe(ofType(endAction))), // cancel
switchMap(() => [action1, action2, action3]), // actions to dispatch if it wasn't canceled.
);
Related
Before fetching new data from the backend, I'm first checking if it's in the store already, and I do take care that the store is always synced every time there's an update.
To do so, my effect does the following:
#Effect()
LoadImage: Observable<LoadImageSuccess> = this.actions$.pipe(
ofType(AppActionTypes.LOAD_IMAGE),
withLatestFrom(action => this.store$.select(selectImage, action.payload).pipe(map(result => ({ action, result })))),
switchMap(observable => observable),
mergeMap(({ action, result }) =>
result !== null
? of(result).pipe(map(image => new LoadImageSuccess(image)))
: this.imageApiService.getImage(action.payload).pipe(
map(image => new LoadImageSuccess(image)),
catchError(error => EMPTY)
)
)
);
This seems to work fine, but I wonder if it can be made nicer:
The switchMap(observable => observable), doesn't look very nice.
Would the condition in the mergeMap be better handled via a iif?
When two components dispatch a LOAD_IMAGE action, my API call is still happening twice, because the first request hasn't completed yet (and hasn't put the image in the store) when the second request starts. This is not a common occurrence with images on my website, but might be with other components in the future and I wonder if there's a way to improve this.
Thanks!
Please note, if I remove the switchMap(observable => observable),, I get the following error:
The object that gets passed to the mergeMap if I comment out the switchMap, is of type Store:
EDIT:
Based on the accepted answer, this is what I ended up with:
#Effect()
LoadImage: Observable<LoadImageSuccess> = this.actions$.pipe(
ofType(AppActionTypes.LOAD_IMAGE),
mergeMap(action =>
this.store$.select(selectImage, action.payload).pipe(
mergeMap(imageFromStore =>
imageFromStore !== null
? of(imageFromStore).pipe(map(image => new LoadImageSuccess(image)))
: this.imageApiService.getImage(action.payload).pipe(
map(image => new LoadImageSuccess(image)),
catchError(error => EMPTY)
)
)
)
)
);
The problem is that the arg passed to withLatestFrom is the projection function, so that's why you have to resort to the switchMap hack.
If you need to check based on the current action, I think you'd be better off doing it with something like this:
ofType(...),
// do the check
switchMap(
action => this.store.select(...)
.pipe(
...,
// act based on the check's result
// decided to nest this as a response to question 3
// since if multiple calls are made at the same time, only the last one will be good to go
switchMap(({ action, result }) => ...)
)
),
I'm new to RxJs and need help/understanding for the following.
I have page that displays current covid cases. I have it setup to poll every 60 seconds. What I'm trying to understand is, if I subscribe to this observable via another new component, I have wait until the next iteration of 60 seconds is complete to get the data. My question is, if I want to share, is there any way to force to send the data and restart the timer?
I don't want 2 different 60 second intervals calling the API. I just want one, and the interval to restart if a new subscriber is initialized. Hope that makes sense.
this.covidCases$ = timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
map(data => {
return data.cases;
}),
),
),
retry(),
share(),
);
I think this should work:
const newSubscription = new Subject();
const covidCases$ = interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
switchMap(() =>
this.covidService.getCovidCases().pipe(
/* ... */
),
),
takeUntil(this.stopPolling),
shareReplay(1),
src$ => defer(() => (newSubscription.next(), src$))
);
I replaced timer(1, 60 * 1000) + retry() with interval(60 * 1000).
My reasoning was that in order to restart the timer(the interval()), we must re-subscribe to it. But before re-subscribing, we should first unsubscribed from it.
So this is what these lines do:
interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
/* ... */
)
We have a timer going on, until newSubscription emits. When that happens, takeUntil will emit a complete notification, then it will unsubscribe from its source(the source produced by interval in this case).
repeat will intercept that complete notification, and will re-subscribe to the source observable(source = interval().pipe(takeUntil())), meaning that the timer will restart.
shareReplay(1) makes sure that a new subscriber will receive the latest emitted value.
Then, placing src$ => defer(() => (newSubscription.next(), src$)) after shareReplay is very important. By using defer(), we are able to determine the moment when a new subscriber arrives.
If you were to put src$ => defer(() => (console.log('sub'), src$)) above shareReplay(1), you should see sub executed logged only once, after the first subscriber is created.
By putting it below shareReplay(1), you should see that message logged every time a subscriber is created.
Back to our example, when a new subscriber is registered, newSubscription will emit, meaning that the timer will be restarted, but because we're also using repeat, the complete notification won't be passed along to shareReplay, unless stopPolling emits.
StackBlitz demo.
This code creates an observable onject. I think what you should do is to add a Replaysubject instead of the Observable.
Replaysubjects gives the possibility to emit the same event when a new subscription occurs.
timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
tap(result => {
if (!result.page.totalElements) {
this.stopPolling.next();
}
}),
map(data => {
return data.cases;
}),
tap(results =>
results.sort(
(a, b) =>
new Date(b.covidDateTime).getTime() -
new Date(a.covidDateTime).getTime(),
),
),
),
),
retry(),
share(),
takeUntil(this.stopPolling),
).subscribe((val)=>{this.covidcases.next(val)});
This modification results in creating the timer once so when you subscribe to the subject it will emit the latest value immediately
You can write an operator that pushes the number of newly added subscriber to an given subject:
const { Subject, timer, Observable } = rxjs;
const { takeUntil, repeat, map, share } = rxjs.operators;
// Operator
function subscriberAdded (subscriberAdded$) {
let subscriberAddedCounter = 0;
return function (source$) {
return new Observable(subscriber => {
source$.subscribe(subscriber)
subscriberAddedCounter += 1;
subscriberAdded$.next(subscriberAddedCounter)
});
}
}
// Usage
const subscriberAdded$ = new Subject();
const covidCases$ = timer(1, 4000).pipe(
takeUntil(subscriberAdded$),
repeat(),
map(() => 'testValue'),
share(),
subscriberAdded(subscriberAdded$)
)
covidCases$.subscribe(v => console.info('subscribe 1: ', v));
setTimeout(() => covidCases$.subscribe(v => console.info('subscribe 2: ', v)), 5000);
subscriberAdded$.subscribe(v => console.warn('subscriber added: ', v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
Future possibilities:
You can update the operator easily to decrease the number in case you want to react on unsubscribers
!important
The takeUnit + repeat has already been postet by #AndreiGătej. I only provided an alternative way for receiving an event when a subscriber is added.
Running stackblitz with typescript
If the subscriberAdded operator needs some adjustements, please let me know and I will update
I have an input element, where I want to show an autocomplete solution. I try to control the HTTP request's number with RxJS. I want to do: the HTTP request start only after 1 second of the user stop the typing.
I have this code:
of(this.inputBox.nativeElement.value)
.pipe(
take(1),
map((e: any) => this.inputBox.nativeElement.value),
// wait 1 second to start
debounceTime(1000),
// if value is the same, ignore
distinctUntilChanged(),
// start connection
switchMap(term => this.autocompleteService.search({
term: this.inputBox.nativeElement.value
})),
).subscribe((result: AutocompleteResult[]) => {
console.log(result);
});
The problem is the debounceTime(1000) didn't wait to continue the pipe. The switchMap start immediately after every keyup event.
How can I wait 1 second after the user finishes typing?
The problem is that your chain starts with of(this.inputBox.nativeElement.value).pipe(take(1), ...) so it looks like you're creating a new chain (with a new debounce timer) on every single key press.
Instead you should have just one chain and push values to its source:
const keyPress$ = new Subject();
...
keyPress$
.pipe(
debounceTime(1000),
...
)
...
keyPress$.next(this.inputBox.nativeElement.value);
Why don't you create a stream with fromEvent?
I didn't find necessary to use distinctUntiChanged as the input event only triggers when a change occurs (i.e. the user adds/removes stuff). So text going through the stream is always different.
const {fromEvent} = rxjs;
const {debounceTime, map} = rxjs.operators;
const text$ =
fromEvent(document.querySelector('input'), 'input')
.pipe(
debounceTime(1000),
map(ev => ev.target.value));
text$.subscribe(txt => {
console.log(txt);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.min.js"></script>
<input/>
I have a class, QueueManager, which manages some queues.
QueueManager offers 3 APIs
deleteQueue(queueName: string): Observable<void>
createQueue(queueName: string): Observable<string>
listQueues(): Observable<string>: Observable`
deleteQueue is a fire-and-forget API, in the sense that it does not return any signal when it has completed its work and deleted the queue. At the same time createQueue fails if a queue with the same name already exists.
listQueues() returns the names of the queues managed by QueueManager.
I need to create a piece of logic which deletes a queue and recreates it. So my idea is to do something like
call the deleteQueue(queueName) method
start a loop calling the listQueues method until the result returned shows that queueName is not there any more
call createQueue(queueName)
I do not think I can use retry or repeat operators since they resubscribe to the source, which in this case would mean to issue the deleteQueue command more than once, which is something I need to avoid.
So what I have thought to do is something like
deleteQueue(queueName).pipe(
map(() => [queueName]),
expand(queuesToDelete => {
return listQueues().pipe(delay(100)) // 100 ms of delay between checks
}),
filter(queues => !queues.includes(queueName)),
first() // to close the stream when the queue to cancel is not present any more in the list
)
This logic seems actually to work, but looks to me a bit clumsy. Is there a more elegant way to address this problem?
The line map(() => [queueName]) is needed because expand also emits values from its source observable, but I don't think that's obvious from just looking at it.
You can use repeat, you just need to subscribe to the listQueues observable, rather than deleteQueue.
I've also put the delay before listQueues, otherwise you're waiting to emit a value that's already returned from the API.
const { timer, concat, operators } = rxjs;
const { tap, delay, filter, first, mapTo, concatMap, repeat } = operators;
const queueName = 'A';
const deleteQueue = (queueName) => timer(100);
const listQueues = () => concat(
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['B'])),
);
const source = deleteQueue(queueName).pipe(
tap(() => console.log('queue deleted')),
concatMap(() =>
timer(100).pipe(
concatMap(listQueues),
tap(queues => console.log('queues', queues)),
repeat(),
filter(queues => !queues.includes(queueName)),
first()
)
)
);
source.subscribe(x => console.log('next', x), e => console.error(e), () => console.log('complete'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.js"></script>
I'd like to sequence multiple async actions and when they are all finished, emit final actions:
const initializeEpic = (action$, state$, dependencies) =>
action$.ofType("INITIALIZE_APP_REQUEST").pipe(
mergeMap((action) =>
[
getUserRequest(),
getCoreDataRequest(),
// many other requests ...
// now here I want to subscribe to GET_USER_SUCCESS,
// GET_CORE_DATA_SUCCESS and other request "success" type's and
// once all are called, call initializeApp.success()
// keep in mind that "..._SUCCESS" actions are called from
// corresponding getUserEpic, getCoreDataEpic, etc...
]
)
)
I'm not sure if I'm trying to implement epic composition with good approach, but my idea in short is:
Call parent epic with PARENT_REQUEST action
Call child epics via corresponding *_CHILD_REQUEST action (child epics would catch it via action$.ofType(*_CHILD_REQUEST))
Subscribe to *_CHILD_SUCCESS action within parent epic (which is dispatched from child epic)
Once all *_CHILD_SUCCESS actions are called, call PARENT_SUCCESS
I believe you are looking for forkJoin. You can do something like this:
const loadEpic = (action$, state$) =>
action$.ofType("INITIALIZE_APP_REQUEST").pipe(
mergeMap((action) =>
forkJoin([
getUserRequest(),
getCoreDataRequest()
]).pipe(
map(([users, coreData]) =>
initializeApp.success(users, coreData)
)
)
)
)
Please note that the requests are done simultaneously and not sequentially. You don't have to make it in sequence as long as they don't depend on each other.
Also, getUserRequest() and getCoreDataRequest() must return an observable, for eg:
function getUserRequest() {
return from(axios.get(`/endpoint`)).pipe(map(response => response.data));
}