switch Observable stream and grab subscription each next - javascript

I have an Observable listening to the URL and I am switching it to a getRows() which returns an Observable pulling data back from the API with the URL parameters. I want to be able get a Subscription reference for every emit that getRows() does. This is to show a loading indicator on the UI.
The current code:
this.tableSource = this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.map(p => this.getRows(p))
.switch()
.share();
And then explicitly when I have changed the parameters I have been calling:
this.tableLoad = this.tableSource.take(1).subscribe((r) => this.rows = this.parseRows(r));
But I want to enable the component to update when external entities manipulate the URL and so I should be subscribing instead of sharing tableSource, so how can I get a Subscription everytime I call getRows(), is it possible?

I managed to solve it this way:
this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.do(p => {
this.tableLoad = this.getRows(p).subscribe((r) => this.rows = this.parseRows(r));
})
.subscribe();
So I stopped trying to use both Observable sequences as one (even though one depends on the other), and just subscribed to the second as a side effect of the first.

Related

What is the proper usage of asynchronoscity in an Angular app with injected service?

My goal is to implement the ability to delete a specific document in a firebase collection, as a button in a component. To do this, I'm using an Angular service to get all the document IDs in a collection, as shown below (because the IDs also have utility in other components):
getCurrUserDocID(){
this.getUsers().subscribe(arr =>{
this.listUsers = arr.map(item => {
return{
userDocID : item.payload.doc.id,
...item.payload.doc.data() as User
}
})
})
return{
userIDs : this.listUsers
};
Injecting this service into components does allow me to get the docIDs. However, it's finicky. When I called it in ngOnInit in a component that loads under a navbar tab, it doesn't work unless I click that tab a couple times:
ngOnInit(): void {
console.log(this.service.getCurrUserDocID().userIDs)
}
The first and second click return an empty array in the console, while the third finally returns the actual array of data. I suspect this has something to do with observables and asychronous-ity.
You are using a subscription to get the data from this.getUsers() observer.
On getting new data you set the value of this.listUsers.
Now all you need in your component is to access this.listUsers.
Just remember that every time there will be data coming from this.getUsers() observer the value of this.listUsers will be overwritten which will cause your component to rerender.
The fact you get the data only on the third invocation may be related to the time it takes for this.getUsers() to return date or to the way you use it.
You have to notice that the subscription to the observable will return multiple results.
Don't subscribe out of component becouse you will need to unsubscribe it. If you want to sort data use map() operator in pipe().
getCurrUserDocID(){
this.listUsers = this.getUsers().pipe(
map(item => {
return {
userDocID: item.payload.doc.id,
...item.payload.doc.data() as User
}
})
)
};
In component
constructor(public: service: Service) {} // Make it public so you can use it in html file
ngOnInit(): void {
this.service.getCurrUserDocID() // here just triger `getUsers()` function.
}
In html template with async pipe
<ul>
<li *ngFor="let item of service.listUsers | async">
{{item.id}}
</li>
</ul>
Async pipe in template are subscribing and unsubscribing for you so you no need to worry about data leaks.

react-query getting old data

I have a master page that is a list of items, and a details page where I fetch and can update an Item. I have the following hooks based upon the react-query library:
const useItems = (options) => useQuery(["item"], api.fetchItems(options)); // used by master page
const useItem = id => useQuery(["item", id], () => api.fetchItem(id)); // used by details page
const useUpdateItem = () => {
const queryClient = useQueryClient();
return useMutation(item => api.updateItem(item), {
onSuccess: ({id}) => {
queryClient.invalidateQueries(["item"]);
queryClient.invalidateQueries(["item", id]);
}
});
};
The UpdatePage component has a form component that takes a defaultValue and loads that into it's local "draft" state - so it's sort of "uncontrolled" in that respect, I don't hoist the draft state.
// UpdatePage
const query = useItem(id);
const mutation = useUpdateItem();
return (
{query.isSuccess &&
!query.isLoading &&
<ItemForm defaultValue={query.data} onSubmit={mutation.mutate} />
}
);
The problem is after I update, go to Master page, then back to Details page, the "defaultValue" gets the old item before the query completes. I do see it hitting the API in the network and the new value coming back but it's too late. How do I only show the ItemForm after the data is re-queried? Or is there a better pattern?
My updateItem API function returns the single updated item from the server.
I used setQueryData to solve this.
const useUpdateItem = () => {
const queryClient = useQueryClient();
// Note - api.updateItem is return the single updated item from the server
return useMutation(item => api.updateItem(item), {
onSuccess: data => {
const { id } = data;
// set the single item query
queryClient.setQueryData('item', id], data);
// set the item, in the all items query
queryClient.setQueryData(
['item'],
// loop through old. if this item replace, otherwise, don't
old => {
return old && old.map(d => (d.id === id ? data : d));
}
);
}
});
};
I will say, react-query is picky about the key even if it is fuzzy. Originally my id was from the url search params and a string, but the item coming back from the db an int, so it didn't match. So a little gotcha there.
Also, when I go back to the Master list page, I see the item change, which is kind of weird to me coming from redux. I would have thought it was changed as soon as I fired the synchronous setQueryData. Because I'm using react-router the "pages" are complete remounted so not sure why it would load the old query data then change it.
isLoading will only be true when the query is in a hard loading state where it has no data. Otherwise, it will give you the stale data while making a background refetch. This is on purpose for most cases (stale-while-revalidate). Your data stays in the cache for 5 minutes after your detail view unmounts because that’s the default cacheTime.
Easiest fix would just set that to 0 so that you don’t keep that data around.
You could also react to the isFetching flag, but this one will always be true when a request goes out, so also for window focus refetching for example.
Side note: invalidateQueries is fuzzy per default, so this would invalidate the list and detail view alike:
queryClient.invalidateQueries(["item"])
I had the same issue today. After scanning your code it could be the same issue.
const useItem = id => useQuery(["item", id], () => api.fetchItem(id)); // used by details page
The name of the query should be unique. But based on you details the ID changes depends on the item. By that you call the query "item" with different IDs. There for you will get the cached data back if you have done the first request.
The solution in my case was to write the query name like this:
[`item-${id}`...]

how to pipe an observable without creating a new instance

Lets say i have a subject that represents pages in a paginateable table:
pageCount$
I then pipe that into a new variable which goes of to an API to get the data for the table:
const tableData$ = pageCount$.pipe(switchMap(pageCount => getTableData(pageCount)));
This works great, every time i emit a new page tableData$ emits the data for that page.
Now here comes the problem and the question i wish to solve.
On my page i also wish to use that table data to display averages of what it currently contains. So i thought i could just pipe tableData$ through a map that performs those averages.
const averageData$ = tableData$.pipe(map(data => performAverages(data)));
This works, but because every-time i subscribe to each variable it creates a new instance my API call happens twice. Once for the tableData$ subscription and once for the averageData$ subscription. I understand this behavior is by design however.
It feels like i want to use some sort of tap/fork operator but i don't think such an operator exists.
Is it even possible to perform these tasks whilst only making the api call once?
Thanks
You can use the share operator to achieve this.
First create the observable that calls the API, and pipe it with share
https://www.learnrxjs.io/learn-rxjs/operators/multicasting/share.
Then the resulting observable can be subscribed twice, both subscription will receive the same results, without the shared observable being called twice ('multicasting').
That should give you something along the lines of :
const tableData$ = pageCount$.pipe(
switchMap(pageCount => getTableData(pageCount)),
tap(_ => console.log('API called')),
share()
);
// subscribe to tabledata$ twice
tableData$.subscribe(_ => console.log('get and use data once'));
tableData$.subscribe(_ => console.log('get and use data a second time'));
(to be tested, feedback appreciated!)
What about something like this
const tableData$ = pageCount$.pipe(
switchMap(pageCount => getTableData(pageCount).pipe(
map(data =>
const avg = performAverages(data);
return {data, avg}
)
))
);
This way you get an object containing both table data and its average, which is what I understand you are looking for.

How to continuously sample data based on clicks in RxJS?

I have an observable with interesting data that changes over time (BehaviorSubject):
const obs = ...
Then I have a click action (Subject which gets called .next() upon button press):
const clickAction = ...
I wish to do this:
obs.pipe(sample(clickAction)).subscribe(data => console.log(data))
The problem I have is this: when I click my button multiple times in a row, it only logs the message once to the console. If obs changes, then clicking again will once again log to the console.
Expected behavior: the data (whether it has changed or not) should also be logged every time I click the button.
I thought sample was supposed to do this, but I reckon there must be a better way to do this?
sample isn't working in your case because:
sample looks at the source Observable and emits whichever value it
has most recently emitted since the previous sampling, unless the
source has not emitted anything since the previous sampling.
withLatestFrom
Use withLatestFrom to combine click events with the latest emitted value from your source observable.
clickAction.pipe(
withLatestFrom(obs),
map(([notifier, source]) => source)
).subscribe(console.log)
https://stackblitz.com/edit/typescript-uxizci
I ended up implementing this:
export function upon<T>(notifier: Observable<any>): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
const latest = withLatestFrom(source)(notifier)
const mapper = map(([, source]: [any, T]) => source)
return mapper(latest)
}
}
Which can be used like so: obs.pipe(upon(clickAction))

How make secondary HTTP request based upon results of first

I have two async requests I am trying to fulfill, the second based upon the results of the first. The way I am trying to do this is by:
Listen for success of first action: actions.GetAllItems
Select out from the store the relevant items based on ID: this.store.select(selectors.getItemsById)
Map over the returned IDs so I can make the second call for each item in the array of IDs returned by the first call
Put results in redux store, render to view.
The way I have now does successfully put it in my redux store. However since it's just vanilla Array.map it doesn't return an observable. Which means the observable isn't stored in this.details$, which means it does not render in my template with {{ details$ | async | json }}
How can I achieve this secondary XHR call based upon the results of the first?
ngOnInit() {
this.store.dispatch(new actions.GetAllItems())
this.details$ = this.actions$.pipe(
ofType(actions.types.GetAllItemsSuccess),
mergeMap(() => {
return this.store.select(selectors.getItemsById); // filter to multiple items based on item ID
}),
map((items: models.IItemGeneralResponse[]) => {
items.map(item => { // sync map does not seem like it belongs in rxjs
this.store.dispatch(
new actions.GetItemDetail(item.id)
);
});
})
);
}
You are trying to do ngrx effects stuff in your angular component. Use effects to handle side effects (calls to the backend/fetching data from local storage etc...) and make your component to watch for a piece of your state via a selector. Let's summarize like this -
Your component [or your guard or resolver] will just dispatch an action to the store.
If you set up a reducer for that action then your reducer will be called first otherwise it will go to step 3
In your effect, you are watching for the dispatched action. Your effect will make the first call and then from the response of the first call, it will make the second call and then it will update the state in your store [or piece of the state] which is being watched by your component by dispatching the respective actions to the store.
This is a typical workflow [It may vary as per the need of the app but the basic idea remains the same]. So keeping the basic idea lets modify your code like this -
In your component
sliceOfState$: Observable<any>; //change the type of observabe as per your app
ngOnInit() {
this.store.dispatch(new actions.GetAllItems())
//this observable will be used to render your data on UI
//you can use various rxjs operators to transform your data before shoing to UI
this.sliceOfState$ = this.store.select(//your selector which gives you sliceOfState);
}
Now In your effect -
#Effect()
this.details$ = this.actions$.pipe(
ofType(actions.types.GetAllItems),
switchMap(() => {
//here you call API which makes the call to backend which return allItems
return this.yourServiceWhichGetAllItems.getAllItems();
}),
switchMap(allItems => {
//now for each item you need to get its detail
//so forkJoin all the observables which calls the backedn for each item
const obs$ = allItems.map(item => this.yourServiceWhichGetDetails.getItemDetail(item));
return forkJoin(obs$);
})
map(allItemsWithDetails => {
//here you should call your action which will update the state in your store
return new actions.SetAllItemsDetails(allItemsWithDetails);
})
);
I have provided pseudo code which will give you an idea of how to achieve what you want to do. For more info, you can visit the official site of ngrx - https://ngrx.io/guide/effects

Categories