Angular Subscription called twice - javascript

I have a chain of observables which follow this logic:
getStyles() --> getPrices()
For every config.id in configs, getStyles() returns a style Object, this style Object is passed to getLease where a price is appended to it and is then pushed an array called "style"
getStyles(configs: any) {
configs.forEach(config => {
this._APIService.getStyleByID(config.id).subscribe(
res => {
res.config = config;
this.getLease(res);
}
);
});
}
getLease(style: any): void {
this._priceService.getPrice().subscribe(
price => {
style.price = price;
this.garage.push(style);
console.log(this.garage);
});
}
}
The issue I am experiencing is that there is a duplicate call being made because the style array has 2x as many style objects as it needs to have. The issue being that Angular is calling this._APIService.getStyleByID() twice when I expect it to call once. How do I fix my Observables to only be called once?

Try switchMap, it's like mergeMap but it cancels the last request if it changes:
getStylesWithoutYear(): void {
this._APIService.getStylesWithoutYear()
.switchMap(styles => {
styles.years.forEach(year => {
year.styles.forEach(style => {
this._APIService.getStyleByID(style.id)
})
})
})
.subscribe(style => {
console.log('style', style)
})
}

Related

How to search an array of objects obtained by axios for an id? Vue 2

I am trying to verify if the user is inside that list that I capture by axios, the issue is that I have used the FILTER option but it always returns undefined or [], being that if the user exists in that array.
I can't think what else to do, because I validate if it is by console.log() the variable with which I ask and if it brings data.
created() {
this.getStagesDefault()
this.getSalesman()
this.getStagesAmountByUser()
},
methods: {
async getSalesman(){
const { data } = await axios.get('salesman')
this.employees = data.data
},
getStagesAmountByUser(){
console.log(this.user['id'])
var objectUser = this.employees.filter(elem => {
return elem.id === this.user['id']
})
console.log(objectUser)
},
Console
Vue data
The method getSalesman is asynchronous, meaning that getStagesAmountByUser will start executing before getSalesman finishes.
Two ways to fix the problem:
Await the getSalesman method, but you have to make the created method async as well. Change the code as follows:
async created() {
this.getStagesDefault()
await this.getSalesman()
this.getStagesAmountByUser()
}
Attach a .then to the getSalesman function, and start the next one inside the .then. Change the code as follows:
created() {
this.getStagesDefault()
this.getSalesman().then(() => this.getStagesAmountByUser())
}
getSalesman is an async method. At the time of the filter, the array being filtered is still empty.
this.getSalesman() // this runs later
this.getStagesAmountByUser() // this runs right away
Have the methods run sequentially by awaiting the async method:
await this.getSalesman()
this.getStagesAmountByUser()
You can avoid the inefficient clientside filtering if you pass the id to the backend and only select by that id.
Additionally, created only gets called once unless you destroy the component which is also inefficient, so watch when user.id changes then call your method again.
Plus don't forget you must wrap any async code in a try/catch else you will get uncaught errors when a user/salesman is not found etc, you can replace console.error then with something which tells the user the error.
{
data: () => ({
employee: {}
}),
watch: {
'user.id' (v) {
if (v) this.getEmployee()
}
},
created() {
this.getEmployee()
},
methods: {
getEmployee() {
if (typeof this.user.id === 'undefined') return
try {
const {
data
} = await axios.get(`salesman/${this.user.id}`)
this.employee = data.data
} catch (e) {
console.error(e)
}
}
}
}

Array is not array anymore when passed into Vuex action function

EDIT: Added extra code in the filterEvents snippet for more context.
I'm not quite understanding what's going on with my code. I'm trying to pass an array into an action function inside of my Vuex store. If I return a Promise inside of that action function, then the parameter being passed isn't of type Array and is instead an Object, which results in the reject() error that I have for the Promise.
Here's some code for context:
filterEvents({ commit }, events) {
console.log(Array.isArray(events)); //this ends up false
console.log(events);
return new Promise((resolve, reject) => {
if (!Array.isArray(events)) {
reject("Invalid argument: is not of type Array.");
}
let filtered = events.filter((event) => {
let now = new Date();
let event_stop = new Date(event.stop_time);
if (event_stop >= now || event_stop == null) {
return event;
}
});
resolve(filtered);
});
}
Here's where I call filterEvents; inside of getEvents;
getEvents({ state, commit, dispatch }, searchParams) {
.....
eventful.getEvents(searchParams).then(async (res) => {
.....
console.log(Array.isArray(res.data.events.event)); //this ends up true
console.log(res.data.events.event);
/* where I call it */
await dispatch("filterEvents", res.data.events.event).then((res) => {
.....
});
}).catch((err) => {
.....
});
}
Here's the output from the Chrome developer console. First two outputs are from getEvents and last two are from filterEvents
Would really like an explanation as to why this is the case. I'm going to bet it's something small, but it's 3 a.m. at the moment and my brain can't wrap around why it's not of type Array when passed into filterEvents.
I always try to check the length prop of the array which helps me out in such cases.
...
return new Promise((resolve, reject) => {
if (!Array.isArray(events) && !events.length) {
reject("Invalid argument: is not of type Array.");
}
.....
});
...
I finally understood what my issue was after taking another look at the object that was being logged on the console. I did not know that Vuex actions HAD to have two arguments if you want to pass in a payload into that function. For example, I initially did this
filterEvents(events) {
.....
}
but what I really needed to do was
filterEvents(context, events) {
.....
}
The context argument is the object that allows you to do things such as commit and dispatch. I usually destructure the context object (i.e. { commit, dispatch} ), so I for some reason never thought twice about it. You don't have to destructure the context object to use commit and dispatch; if you don't it would just be like
context.commit('function', payload);

How to check the length of service that return an array and map this value as a pipe line?

I have this pipe line but in this point, is not efficient because is causing not reachable code in order to dispatch the action, I am thinking on a tap. What is the better way to integrate the action in a pipeline?
 map((myStore) => {
return myStore.getUsers().map(({name, lastName}) => {
return {
name: name,
sureName: lastName
}            
});
if (myStore.getUsers().length === 0) {
this.store.dispatch(new saveNameAction('James', 'Smith'));
}
})
.subscribe();
I would do it in the subscribe itself:
Something like this:
map((myStore) => {
return myStore.getUsers().map(({name, lastName}) => {
return {
name: name,
sureName: lastName
}
});
})
.subscribe(myModifiedStore => {
console.log(myModifiedStore.getUsers());
if (myModifiedStore.getUsers().length === 0) {
this.store.dispatch(new saveNameAction('James', 'Smith'));
}
});

How to wait for data from api to be loaded and then perform another action in the UI?

I have a method that gets the userlist from the api and populates the list component. Now what i want to do
is highlight the first element of the list once the list is loaded from the api.
I have tried the following approach and it is working fine with setTimeout but how can i make it happen without setTimeout function
Here is the code that i tried,
public getUserList(){
return this.http.get('https://jsonplaceholder.typicode.com/users').pipe(tap(x => {
this.lists = x;
}))
.subscribe((data) => {
setTimeout(() => {
this.clickFirstList();
}, 1000);
});
}
clickFirstList() {
    Array.from(this.ulElement.nativeElement.children).forEach((element: HTMLElement, index: number) => {
      if (index === 0) {
console.log(element);
        element.click();
      }
    });
}
Here is the working code on stackblitz. https://stackblitz.com/edit/angular-brpz2j?file=src/app/search-list.component.ts
You shouldn't manipulate the DOM like that, there is no need. We use angular bindings, and by using those, we can also drastically reduce the code you are currently having. So remove all those DOM manipulations.
First of all we can omit the lists call from either parent or child. Now you are performing the get request twice, which doesn't really make sense. I'm keeping the get-request in the child. We can also make the request in OnInit, why would we need to do it in AfterViewInit?
ngOnInit() {
this.getUserList();
}
public getUserList() {
return this.http
// DONT USE 'ANY', type your data using interface or class
.get<any[]>("https://jsonplaceholder.typicode.com/users")
.subscribe(data => {
this.lists = data;
// you want to emit initially the first item to parent via output
// do it here.
this.onClickList(0, this.lists[0] || {})
});
}
Also we introduce a new variable selectedIndex, which we will change based on the clicked item. Like Jens has done in his answer. You want the first item to be active intially we above pass 0 as the index to the onClick function:
public onClickList(i: number, item: any) {
this.selectedIndex = i
this.selectedList.emit(Object.assign({}, item));
}
We add that to template like so:
<ng-container *ngFor="let item of lists | filter: searchText; index as i">
<li [class.active]="i === selectedIndex"
(click)="onClickList(i, list)">
{{ list.name }}
</li>
</ng-container>
Your forked STACKBLITZ
As mentioned as a comment inside the code. DON'T use any, it completely defeats the purpose of TypeScript, so type your data either with interfaces or classes. I prefer interfaces.
you can use following code
public getUserList(){
return this.http.get('https://jsonplaceholder.typicode.com/users').pipe(tap(x => {
this.lists = x;
}))
.subscribe((data) => {
},
(err) => {
// code to execute if request fails
},
() => {
this.clickFirstList();
}
});
}
Tap also accepts an object map to log next, error, and complete
public getUserList(){
return this.http.get('https://jsonplaceholder.typicode.com/users')
.pipe(
tap( {
next: x => {
console.log('on next', x);
},
error: error => {
console.log('on error', error.message);
},
complete: () => {
console.log('on complete')
this.clickFirstList()
}
}))
.subscribe((x) => {
console.log(x)
});
}
I would remove clickFirstList. And use css and angular.
I dont now what html element you loop on but lets say its a list
ex: html
<ul>
<li *ngFor="let user of list:let index = index" [class.selected]="index===selectedIndex" (click)="onClick(index)>
{{user.Name}}
</li>
</ul>
and the css
.selected{
background-color: yellow;
}
and in typescript component:
selectedIndex = 0; //Default value.
onClick(index:number){
this.selectedIndex=index;
}

How to handle vuex store to fetch data from rest api?

I'm using Vuex to handle my application state.
I need to make an Ajax Get request to a rest api and then show some objects list.
I'm dispatching an action that loads this data from the server but then I don't know how to handle it on the component.
Now I have this:
//component.js
created(){
this.$store.dispatch("fetch").then(() => {
this.objs = this.$store.state.objs;
})
}
But I don't think that the assignment of the incoming data to the local property is the correct way to handle store data.
Is there a way to handle this better? Maybe using mapState?
Thanks!
There are many ways you can do it, you must experiment and find the one that fits your approach by yourself. This is what I suggest
{ // the store
state: {
something: ''
},
mutations: {
setSomething (state, something) {
// example of modifying before storing
state.something = String(something)
}
},
actions: {
fetchSomething (store) {
return fetch('/api/something')
.then(data => {
store.commit('setSomething', data.something)
return store.state.something
})
})
}
}
}
{ // your component
created () {
this.$store
.dispatch('fetchSomething')
.then(something => {
this.something = something
})
.catch(error => {
// you got an error!
})
}
}
For better explanations: https://vuex.vuejs.org/en/actions.html
Now, if you're handling the error in the action itself, you can simply call the action and use a computed property referencing the value in the store
{
computed: {
something () { // gets updated automatically
return this.$store.state.something
}
},
created () {
this.$store.dispatch('loadSomething')
}
}

Categories