Reactive Loading state management with RxJs - javascript

A classic task when you have some input field and you have to fetch something on values changes. Let's imagine we use Angular Reactive Forms. Example:
orders$ = inputControl.valueChanges.pipe(
switchMap((value) => {
return someService.fetch(value);
})
);
Now we should also somehow manage loading state. I usually use tap:
orders$ = inputControl.valueChanges.pipe(
tap(() => { loading = true }), // or loading$.next(true) if loading is a subject
switchMap((value) => {
return someService.fetch(value);
}),
tap(() => { loading = false }), // or loading$.next(false) if loading is a subject
);
However it seems we can somehow avoid assigning values in the tap and use RxJs instead.
But I could not find a way to do with it.
For me, the usage of the ideal solution would be
orders$ = <some abstraction here that depends on inputControl.valueChanges and fetching>
loading$ = <some abstraction here that depends on fetching>

Take a look at my library ez-state.
https://github.com/adriandavidbrand/ngx-ez/tree/master/projects/ez-state
https://adrianbrand.medium.com/angular-state-management-using-services-built-with-ez-state-9b23f16fb5ae
orderCache = new EzCache<Order[]>();
order$ = this.orderCache.value$;
loading$ = this.orderCache.loading$;
inputControl.valueChanges.subscribe(value => {
orderCache.load(someService.fetch(value));
});
An EzCache is a glorified behaviour subject that has methods load, save, update and delete and manages state observables like loading$, loaded$, saving$, saved$ etc.
I would personally would have the cache in the service and expose the observables from the service like in that article I wrote.

If you think about the loading state as part of a larger "order search request", then it becomes easier to see how to use RxJS to create an observable that emits the desired overall state.
Instead of having two separate observables, orders$ and loading$, you could have a single observable that emits both pieces of data (since they are always changed at the same time).
We basically want to create an observable that initially emits:
{
isLoading: true,
results: undefined
}
then, after the results are received, emits:
{
isLoading: false,
results: ** orders from api call **
}
We can achieve by using switchMap, map, and startWith:
orderSearchRequest$ = this.searchTerm$.pipe(
switchMap(term => this.searchService.search(term).pipe(
map(results => ({ isLoading: false, results })),
startWith({ isLoading: true, results: undefined })
)),
);
<form [formGroup]="form">
<input formControlName="searchTerm" placeholder="Order Search">
</form>
<div *ngIf="orderSearchRequest$ | async as request">
<div *ngIf="request.isLoading"> loading... </div>
<ul>
<li *ngFor="let order of request.results">
{{ order.description }}
</li>
</ul>
</div>
Here's a working StackBlitz demo.

Related

Combine multiple observables in to a single RxJS stream

It appears I am lacking knowledge on which RxJS operator to resolve the following problem:
In my music application, I have a submission page (this is like a music album). To load the submission, I use the following query:
this.submissionId = parseInt(params['album']);
if (this.submissionId) {
this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.subscribe((submission) => {
//submission loaded here!
});
}
Easy enough! However, once I've loaded the submission, I have to load some auxiliary information such as the current user (to check if they are the artist of the submission) and comments. In order to avoid nested subscriptions, I can modify the above query to use switchMap to switch the query stream to user and comments observables once the submission resolves:
// stream to query for the submission and then switch query to user
this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.pipe(
switchMap(submission => {
this.submission = submission;
return this.auth.user$
})
).subscribe((user) => {
// needs value of submission here
if (user.id == this.submission.user.id) {
//user is owner of submission
}
})
// stream to query for the submission and then switch query to comments
this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.pipe(
switchMap(submission => {
this.comments$ = this.commentsGQL.watch({
submissionId: submission.id //needs submission response here
})
return this.comments$.valueChanges
})
).subscribe((comments) => {
this.comments = comments;
})
Great! I've avoided the nested subscription issue BUT now...the first part of each submission request is identical. Basically, once, the submission is queried, i want to launch off two parallel queries:
a query for the user
a query for the comments
Which RxJS operator can perform such an operation? I suppose the subscribe at the end would emit an array response like:
.subscribe([user, comments] => {
// check if user == submission.user.id here
// also assign comments to component variable here
})
I believe mergeMap is sort of what I need but I'm not sure how to implement that properly. Or is this a case where I should share() the submission query and then build off my parallel queries separately? I'm very curious! Please let me know, thanks!
You can use the RxJS forkJoin operator for this scenario. As stated on the documentation,
When all observables complete, emit the last emitted value from each.
const userQuery$ = this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.pipe(
switchMap(submission => {
this.submission = submission;
return this.auth.user$
})
)
// stream to query for the submission and then switch query to comments
const commentsQuery$ = this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.pipe(
switchMap(submission => {
this.comments$ = this.commentsGQL.watch({
submissionId: submission.id //needs submission response here
})
return this.comments$.valueChanges
})
)
forkJoin(userQuery$, commentsQuery$).subscribe([user, comments] => {
// check if user == submission.user.id here
// also assign comments to component variable here
})
Try:
this.submissionGQL.watch({
id: this.submissionId
}).valueChanges.pipe(
switchMap(submission => {
this.submission = submission;
const user$ = this.auth.user$;
this.comments$ = this.commentsGQL.watch({
submissionId: submission.id
});
return combineLatest(user$, this.comments$);
}),
// maybe put a takeUntil to remove subscription and not cause memory leaks
).subscribe(([user, comments]) => {
// check if user == submission.user.id here
// also assign comments to component variable here
});
Something you should consider is eliminating instance variables with the help of the async pipe given by Angular (https://malcoded.com/posts/angular-async-pipe/).
It will subscribe to the observable, present it into the view and automatically unsubscribe when the view is destroyed.
So, using that, we can get rid of this.submissions = submission by putting:
submissions$: Observable<ISubmission>; // assuming there is an interface of ISubmission, if not put any
// then when this.submissionId is defined
this.submissions$ = this.submissionGQL.watch({
id: this.submissionId
}).valueChanges;
// then when using it in your view you can do {{ this.submissions$ | async }}
The same thing can go for this.comments$. All of this is optional though. I try to minimize instance variables as much as possible when using RxJS because too many instance variables leads to confusion.
Then you can lead off of this.submissions$ observable and subscribe for the other main stream.
this.submission$.pipe(
switchMap(submission => ..... // everything else being the same
)
I chose the combineLatest operator but you can use zip and forkJoin as you see fit. They all have subtle differences (https://scotch.io/tutorials/rxjs-operators-for-dummies-forkjoin-zip-combinelatest-withlatestfrom).

Unable to implement React-table sever data loading

I am testing the React-table server side data to render a huge amount of data fetched from an web api without crashing the browser. With the base react-table settings the browser is unable to handle such amount of records (500000) and crash (it gets stuck in the pending state of the request).
So I find the server side data that maybe can help me.
I followed the instructions from the documentation but typescript is complaining about the data that I am trying to use when I update the state.
This is what I have until now:
The method that fetch the data from web api:
private fetchSales() {
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
this.setState({
sales: data // here I get 500000 items
})
)
}
This fetchSales gets called in the componentDidMount().
Then I have the ReactTable component inside the render():
render() {
const {
sales,
pages
} = this.state;
return (
<div className = "App" >
<ReactTable
data = {sales}
manual
pages = {pages}
defaultPageSize = {10}
onFetchData = {this._fetchData}
columns = {
[{
Header: "Region",
accessor: "Region"
},
{
Header: "Country",
accessor: "Country"
}]
}
/>
</div>
);
}
In the ReactTable there is call to a function called _fetchData and that function looks like this:
private _fetchData(state: any) {
requestData(
state.sales,
state.pageSize,
state.page
).
then(res => {
this.setState({
sales: res.rows, // here typescript complain: "res is of type 'unknown'"
pages: res.pages // here typescript complain: "res is of type 'unknown'"
});
})
}
Inside the setState the res object is type 'unknown' and typescript doesn't like it.
requestData is a function that lives outside the class and get the sales, pageSize and page states:
const requestData = (sales: any, page: number, pageSize: number) => {
return new Promise((resolve, reject) => {
const res = {
rows: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
}
resolve(res);
})
}
The function is almost identical the in the documentation I only removed the filtering and sorting because I don't need them. I only need the res object that return the function.
And I almost forget it, inside the constructor I am attaching the this to the _fetchData method: this._fetchData = this._fetchData.bind(this);
Why is typescript complaining about the res object that I am trying to use to set the state?
Best regards!
Americo
EDIT
I noticed a few mistakes.
private _fetchData(state: any) {
requestData(
state.sales, // that should be state.data (state is the state of ReactTable)
state.pageSize,
state.page
...
Next, I've noticed that you do pagination after fetching the data. But using manual with onFetchData is for handling pagination on the server. For example, with page and pageSize you could pass these parameters to your API. That's the whole point of pagination!
An example from the documentation:
onFetchData={(state, instance) => { //onFetchData is called on load and also when you click on 'next', when you change page, etc.
// show the loading overlay
this.setState({loading: true})
// fetch your data
Axios.post('mysite.com/data', {
//These are all properties provided by ReactTable
page: state.page,
pageSize: state.pageSize,
sorted: state.sorted,
filtered: state.filtered
})
Since you're fetching all of them at once (why though? can't your API on the server handle pagination? this way, you won't have to wait ages before the results are returned), then I suggest you let ReactTable do the work.
That is, you just do:
<ReactTable
columns={columns}
data={data}
/>
ReactTable will take care of pagination. And now, you may use your query to the API in ComponentDidMount.
Could you please instead try to put all the logic in the onFetchData. I could be wrong but it seems to me you misunderstood the instructions in the documentation. OnFetchData is called at ComponentDidMount, it's not telling you that you have to put your function there.
private _fetchData(state, instance) {
const { page, pageSize } = state
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
let sales = data
this.setState({
sales: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
});
)
}
As for Typescript, from what I gather, Typescript doesn't have enough information to infer the type of what your Promise returns.
So you have to explicitly annotate Promises generic type parameter:
return new Promise<{ sales: object; pages: number; }>((resolve, reject) => { ... }

Is it possible for Vue.js to automatically update the view when a third-party JSON is updated?

I'm trying to accomplish the following but I don't even know if it is even possible with Vue as I'm struggling to get the desired result:
I have an endpoint for an API which returns many objects within an array.
I am successfully rendering the data within my Vue application but I wanted to know if it is possible for Vue to "track" when the array has been updated with more objects and then render those in the view.
I am using setInterval to perform a GET request every 10 minutes and the new data is going into the object within my data() correctly but the changes are not reflected within the view.
At the moment I am changing a boolean from true to false at the beginning and end respectively so that the view is rendered again with v-if.
My goal is to create a simple Twitter feed app that performs a GET request every 10 minutes, collects the tweets, puts them into my Vue instance and show them in the view without having to reload the page/re-render the component. Like an automatic Twitter feed that just constantly loads new tweets every 10 minutes.
Is this even possible? I've tried using the Vue.set() method but that hasn't made any difference.
If it's not possible, what would be the best way to implement something similar?
Here is my code:
JavaScript:
new Vue({
el: '#app',
data: {
items: [],
},
created() {
this.load();
setInterval(() => this.load(), 5000);
},
methods: {
load() {
axios.get('https://reqres.in/api/users?page=2')
.then(response => {
this.items = response.data.data;
});
}
}
});
HTML
<div id="app">
<p v-for="item in items">
{{ item.first_name }}
</p>
</div>
CodePen: https://codepen.io/tomhartley97/pen/VwZpZNG
In the above code, if the array is updated by the GET request, the chances are not reflected within the view?
Yes it is possible. The way you need to set new reactive properties in your Vue instance is the following:
For Object properties: Vue.set(this.baseObject, key, value)
The baseObject cannot be a Vue instance or the base data() object, so you will have to declare a container property.
For Array entries use native array methods: e.g. Array.prototype.push().
Using Vue.set(array, arrayIndex, newArrayElement) does not work
Hence, your solution might look something line that:
<script>
export default {
data() {
return {
response: [],
};
},
mounted() {
setInterval = (() => this.getData), 600000);
}
methods: {
async getData() {
const res = await request();
const resLength = res.data.length;
for (let i = 0; i < resLength; i++) {
// check if entry is already in array
const entryExists = this.response.some((entry) => {
return entry.id === res.data[i].id
})
if (!entryExists) {
// this will make the array entries responsive, but not nested Objects
this.response.push(res.data[i]);
// to create nested responsive Objects you will have to set them explicitly
// e.g. Vue.set(this.response[this.response.indexOf(res.data[i])], nestedObjectKey, res.data[i].nestedObject)
}
}
}
}
};
</script>
Well, I view the codepen, I known why your view do not get update: the api response always return the same array!
Try to return different data.
The api returns an array, so the data defines
data() {
return {
array: [] // array that api returns
}
}
The template may look like this
<div v-for="item in array">
</div>
And the update methods
update() {
setInterval(async () => {
let resp = await api()
this.array = resp.data.concat(this.array)
}, TEN_MINUTES)
}

NgRx Store - conditionally loading data based on property of another state object

I have an app that renders fixtures, results etc from my API based on a season id - this id is stored in state as a property of SeasonState:
export interface SeasonsState extends EntityState<Season> {
allSeasonsLoaded: boolean;
currentlySelectedSeasonId: number;
}
This is used by other components to determine which fixtures, results etc to fetch from the API and store in state. For example:
this.store
.pipe(
select(selectCurrentlySelectedSeason)
).subscribe(seasonId => {
this.store.dispatch(new AllFixturesBySeasonRequested({seasonId}));
this.fixtures$ = this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId))
);
});
This works well, but what I'd really like is to be able to only fetch fixtures again fixtures for that particular season are not already stored in state.
I've tried creating a selector to use to conditionally load the data from the API in my effects:
export const selectSeasonsLoaded = (seasonId: any) => createSelector(
selectFixturesState,
fixturesState => fixturesState.seasonsLoaded.find(seasonId)
);
But I am unsure how to implement this / whether this is the right approach.
EDIT: using info from the answer below, I have written the following Effect, however see the comment - I need to be able to use seasonId from the payload in my withLatestFrom.
#Effect()
loadFixturesBySeason$ = this.actions$
.pipe(
ofType<AllFixturesBySeasonRequested>(FixtureActionTypes.AllFixturesBySeasonRequested),
withLatestFrom(this.store.select(selectAllFixtures)), // needs to be bySeasonId
switchMap(([action, fixtures]) => {
if (fixtures.length) {
return [];
}
return this.fixtureService.getFixturesBySeason(action.payload.seasonId);
}),
map(fixtures => new AllFixturesBySeasonLoaded({fixtures}))
);
Have your effect setup like this [I am using ngrx 6 so tested on ngrx 6; If you are using some other version then you will get an idea and adjust the code accordingly] -
#Effect() allFixturesBySeasonRequested: Observable<Action> =
this._actions$
.pipe(
//Please use your action here;
ofType(actions.AllFixturesBySeasonRequested),
//please adjust your action payload here as per your code
//bottom line is to map your dispatched action to the action's payload
map(action => action.payload ),
switchMap(seasonId => {
//first get the fixtures for the seasonId from the store
//check its value if there are fixtures for the specified seasonId
//then dont fetch it from the server; If NO fixtures then fetch the same from the server
return this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId)),
//this will ensure not to trigger this again when you update the fixtures in your store after fetching from the backend.
take(1),
mergeMap(fixtures => {
//check here if fixtures has something OR have your logic to know
//if fixtures are there
//I am assuming it is an array
if (fixtures && fixtures.lenght) {
//here you can either return NO action or return an action
//which informs that fixtures already there
//or send action as per your app logic
return [];
} else {
//NO fixtures in the store for seasonId; get it from there server
return this.http.get(/*your URL to get the fixtures from the backend*/)=
.pipe(
mergeMap(res => {
return [new YourFixtureFetchedSucccess()];
}
)
)
}
})
);
})
)
Now you need to dispatch the action which fetches the fixtures for the specified seasonId from your service/component or the way your app is designed.
Hope it will give you an idea and helps in solving your problem.

Angular 2 Array Subject .next() not invoking another component's observer

I'm using firebase as my backend.
Inside of a data.service.ts, I create a Subject array which will be filled by the firebase observer on app init:
private orders = new Subject<any>();
orders$ = this.orders.asObservable();
firebase.database().ref(this.fbDataPath).on('child_added', (childSnapshot) => {
this.orders.next(
{
key: childSnapshot.key,
name: childSnapshot.val().name,
items: childSnapshot.val().items
}
)
})
I then provide a separate directory component with DataService and subscribe to its orders observable:
DataService.orders$.subscribe(
order => {
console.log('subscribe hit')
})
I can't seem to get the listener component to trigger on a next. I made this work for a boolean isLoggedIn, and I must be missing something in this scenario. Thanks!
It might be because you're using this in a closure. Remove the this from this.orders.next()

Categories