my code outputs everytime different numbers. Is this a proper way I am using it?
Here is the code:
export class GetPlanetsService {
url='https://swapi.co/api/planets/?page=';
planets:Planet[]=[];
headers: HttpHeaders = new HttpHeaders()
.set('Accept', 'application/json');
constructor(private http:HttpClient) { }
getPlanet(pageIndex){
return this.http.get<Planets>(`${this.url}${pageIndex}`,{headers:this.headers});
}
getAllPlanets(){
let numberOfPages=7; // Tried to do it dynamically but got infinite loop
for(let j=1;j<=numberOfPages;j++){
this.getPlanet(j).subscribe(value=>{
for(let i=0;i<value.results.length;i++){
this.planets.push(value.results[i]);
if(j==numberOfPages && i==(value.results.length-1)){
console.log(this.planets); //There is outputted everytime different number
}
}
});
}
}
Have you got any tips and could you explain it in simple words?
Regards
You can use forkJoin for this, Dont forget to include
import { forkJoin } from 'rxjs';
forkJoin waits for each HTTP request to complete and group’s all the
observables returned by each HTTP call into a single observable array
and finally return that observable array.
getPlanet(pageIndex) {
return this.http.get < Planets > (`${this.url}${pageIndex}`, {
headers: this.headers
});
}
getAllPlanets() {
const response = [...Array(7).keys()].map(i => this.getPlanet(i));
return forkJoin(response);
}
in your component you can call getAllPlanets
this.getPlanetsService.getAllPlanets()
.subscribe(res => {
console.log(res);
}, err => {
console.log(err);
});
There are few ways you can control your async behavior.
Promises.all
Async Await
Async library
Ok there is deeper problemes here.
First, why are you trying to call the server 7 times in a row? What if I want page 200? You will make 200 Http requests? The server should return the entire list. It will increase performance and reduce complexity on client side.
Also, why getAllPlanets() return void? It's not intuitive. Instead, getAllPlanets() should return Observable<Planet[]>. All functions should either return of modify (it's part of the CQS principle) here the purpose it to return data so you can't notify your object state e.g. this.planets.push(value.results[i]). What if a invoke the function twice? Then, this.planets will contain the result of both requests.
Related
in my project I'm fetching data from an API (trip entities). This is my model:
//Trip.model.ts
export class Trip{
created_at?:Date;
updated_at?:Date;
.... bunch of fields
}
In my component I'm fetching the data and assigning it to the trips variable. However, when I'm trying to access any of the items in the trips array I get 'undefined'. I also can't loop through it, I tried both forEach and for...in/of.
I tried using an interface instead of a class but with no luck. How can I loop through that array of objects in order to use the data in it?
Component's code:
userName:string='';
trips:Trip[]=[];
moment:any=moment;
usersData:any={};
constructor(private auth: AuthService,
private storage: LocalStorageService,
private translate: TranslateService,
private tripService: TripService){}
ngOnInit(): void {
console.log(this.translate.currentLang)
this.userName=localStorage.getItem('username')!;
this.fetchTrips();
this.fetchPics();
}
fetchTrips() {
this.tripService.getTrips().subscribe({
next: data => {
data[0].data.forEach((value) => {
let trip: Trip = plainToInstance(Trip,value);
this.trips.push(trip);
});
}, error: error => {
console.log(error);
}
});
}
//fetchPics because I want to extract
//user's profile pics' urls from the trips array
fetchPics(){
console.log(this.trips);
console.log(this.trips[0]);
this.trips.forEach((trip)=>{
console.log(trip);
});
}
getTrips service method:
getTrips(){
return this.http.get<any>(Api.API+Endpoints.TRIP);
}
This is what shows when I
console.log(this.trips)
after assignment.
Data from the API:
Pictures cropped to make them more readable.
You are trying to get access to this.trips value before you actually have your data on it.This happens becouse you get that data asynchronously, just inside the subscribe of this.tripService.getTrips()
So, in order to solve your problem, you just need to move this invoke:
this.fetchPics();
inside the subscribe of fetchTrips() method, like this:
fetchTrips() {
this.tripService.getTrips().subscribe({
next: data => {
data[0].data.forEach((value) => {
let trip: Trip = plainToInstance(Trip,value);
this.trips.push(trip);
this.fetchPics();
});
}, error: error => {
console.log(error);
}
});
}
The function fetchPics() executes before your getTrips call ends. You need to call the method only after your HTTP call ends, and after you populate your trips array successfully.
fetchTrips() {
this.tripService.getTrips().subscribe({
next: data => {
//Populate your trips array
data[0].data.forEach((value) => {
let trip: Trip = plainToInstance(Trip,value);
this.trips.push(trip);
});
// this is where you need to call
this.fetchPics();
}, error: error => {
console.log(error);
}
});
}
This is happening because of JS is asynchronous. you are making an http request here, that may take some time to get data from server. let's assume that might take 1 minute untill then compiler will not stop it's execution process In that 1 min of time it will execute next statements.because of that your fetchpics() method is being executed before fecthtrips() execution done. To overcome this we can use async, await as like below.
async fetchTrips() {
this.tripService.getTrips().subscribe({
next: data => await {
data[0].data.forEach((value) => {
let trip: Trip = plainToInstance(Trip,value);
this.trips.push(trip);
});
}, error: error => {
console.log(error);
}
});
}
I need to make 2 AJAX requests to the same endpoint that would return filtered and unfiltered data. Then I need to combine results and use them both in processing.
loadUnfilteredData() {
// remember status
const {status} = this.service.filters;
delete this.service.filters.status;
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processUnfilteredData(appCounts)
);
// restore status
if (status) {
this.service.filters.status = status;
}
}
loadFilteredData() {
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processFilteredData(appCounts)
);
}
The problem is that this.service.saleCounts$() is impure and instead of using arguments just uses this.service.filters.
That's why i have to store the status, then delete it from filter, then do the request, and then restore (because same filter is used by other requests).
So I can't just do combineLatest over two observables (because i need to restore).
Is there any workaround?
(p.s. I know the approach is disgusting, i know about state management and about pure functions. Just wanted to know is there any beautiful solution).
I believe your constraints require that the two operations are run sequentially , one after the other, rather than in parallel as is generally the case when we're using combineLatest.
To run two Observables sequentially, we can use switchMap (as an operator inside a pipe call in modern rxjs):
doFirstOperation()
.pipe(
switchMap(result => return doSecondOperation())
);
One potential issue with that is that you lose access to the result of doFirstOperation when you switchMap it to the result of doSecondOperation. To work around that, we can do something like:
doFirstOperation()
.pipe(
switchMap(firstResult => return doSecondOperation())
.pipe(
map(secondResult => [firstResult, secondResult])
)
);
i.e., use map to change the returned value of switchMap to be an array including both values.
Putting this together with your "disgusting" requirements for state management, you could use something like:
loadData() {
const { status } = this.service.filters;
delete this.service.filters.status;
return this.service
.saleCounts$()
.pipe(
finalize(() => {
if (status) {
this.service.filters.status = status;
}
}),
switchMap(filteredData => {
return this.service
.saleCounts$() // unfiltered query
.pipe(map(unfilteredData => [filteredData, unfilteredData]));
})
)
.subscribe(results => {
const [filteredData, unfilteredData] = results;
this.processFilteredData(filteredData);
this.processUnfilteredData(unfilteredData);
});
}
I'm not too many people would categorize that is beautiful, but it does at least allow you to get results in a way that looks like you used combineLatest, yet works around the constraints imposed by your impure method.
I know it is bad practice to call subscribe within subscribe but I don't know how to handle it differently with my special case.
The code as it is now works, but my problem is that if I update my website for example every second, parts of the table are loaded first and other parts are loaded afterwards (the content of the subscibe within my subscribe).
I have a service containing a function that returns an Observable of a list of files for different assets.
Within that function I request the filelist for each asset by calling another service and this service returns observables.
I then iterate over the elements of that list and build up my data structures to return them later on (AssetFilesTableItems).
Some files can be zip files and I want to get the contents of those files by subscribing to another service (extractZipService). To be able to get that correct data I need the name of the file which I got by requesting the filelist. I then add some data of the zip contents to my AssetFilesTableItems and return everything at the end.
The code of that function is as follows:
getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
const data = assetIds.map(assetId => {
// for each assetId
return this.fileService.getFileList(assetId)
.pipe(
map((datasets: any) => {
const result: AssetFilesTableItem[] = [];
// iterate over each element
datasets.forEach((element: AssetFilesTableItem) => {
// apply regex filter to filename
if (filter.test(element.name)) {
this.logger.debug(`Filter ${filter} matches for element: ${element.name}`);
// build up AssetFilesTableItem
const assetFilesItem: AssetFilesTableItem = {
name: element.name,
type: element.type,
asset: assetId
};
// save all keys of AssetFilesTableItem
const assetFilesItemKeys = Object.keys(assetFilesItem);
// if file is of type ZIP, extract 'config.json' from it if available
if (showConfig && element.type.includes('zip')) {
this.extractZipService.getJSONfromZip(assetId, element.name, 'config.json')
.subscribe((configJson: any) => {
const jsonContent = JSON.parse(configJson);
const entries = Object.entries(jsonContent);
entries.forEach((entry: any) => {
const key = entry[0];
const value = entry[1];
// only add new keys to AssetFilesTableItem
if (!assetFilesItemKeys.includes(key)) {
assetFilesItem[key] = value;
} else {
this.logger.error(`Key '${key}' of config.json is already in use and will not be displayed.`);
}
});
});
}
result.push(assetFilesItem);
}
});
return result;
}));
});
// return combined result of each assetId request
return forkJoin(data);
}
}
I update my table using the following code within my component:
getValuesPeriodically(updateInterval: number) {
this.pollingSubscription = interval(updateInterval)
.subscribe(() => {
this.getAssetfilesFromService();
}
);
}
getAssetfilesFromService() {
this.assetfilesService.getAssetfilesData(this.assetIds, this.filterRegEx, this.showConfig)
.subscribe((assetFilesTables: any) => {
this.assetFilesData = [].concat.apply([], assetFilesTables);
});
}
Edit: I tried ForkJoin, but as far as I understandit is used for doing more requests in parallel. My extractZipService though depends on results that I get from my fileService. Also I have a forkJoin at the end already which should combine all of my fileList requests for different assets. I don't understand why my view is not loaded at once then.
EDIT: The problem seems to be the subscribe to the extractZipService within the forEach of my fileService subscribe. It seems to finish after the fileService Subscribe. I tried lots of things already, like SwitchMap, mergeMap and the solution suggested here, but no luck. I'm sure it's possible to make it work somehow but I'm running out of ideas. Any help would be appreciated!
You are calling this.extractZipService.getJSON inside a for loop. So this method gets called asynch and your function inside map is not waiting for the results. When result does come as your items are same which is in your view they get refreshed.
To solve this you need to return from this.extractZipService.getJSON and map the results which will give you a collections of results and then you do forkJoin on results ( Not sure why you need to forkjoin as there are just the objects and not API's which you need to call )
this.logger.debug(`ConfigJson found for file '${element.name}': ${configJson}`);
const jsonContent = JSON.parse(configJson);
const entries = Object.entries(jsonContent);
entries.forEach((entry: any) => {
// code
});
complete code should look on similar lines :-
getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
const data = assetIds.map(assetId => {
// for each assetId
return this.fileService.getFileList(assetId)
.pipe(
map((datasets: any) => {
// iterate over each element
datasets.forEach((element: AssetFilesTableItem) => {
return this.extractZipService.getJSONfromZip(assetId, element.name,
'config.json')
});
})).map((configJson: any) => {
// collect your results and return from here
// return result
});;
});
// return combined result of each assetId request
return forkJoin(data);
}
}
I have created a Stackblitz(https://stackblitz.com/edit/nested-subscribe-solution) which work along the same lines. You need to use concatMap and forkJoin for getting all the results.
Hope this helps.
I am having trouble to return an observable. It seems like the codes inside the mergeMap is not running at all.
Codes:
book.service.ts
import {HttpClient, HttpHeaders} from '#angular/common/http';
export class bookService {
constructor(
private http: HttpClient,
...others
) {}
addNewBook(book): Observable<Book>{
##### Tried to use mergeMap otherwise the return type won't match
return this.tokenService.getToken().mergeMap((token: string) => {
console.log("Fire this...") <===== This is never output.
const myUrl = "www.testurl.com";
const parameters = {
bookTitle: book.name,
};
return this.http.post<Book>(myUrl, book);
})
}
token.service.ts
public token$: Subject<string>;
..others
public getToken(): Observable<string> {
return this.token$; <= return Observable<string> not Observable<Book>
}
book.component.ts that calls the addNewBook method.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book);
}).then((result) => {
console.log(result);
})
I can't really change the token service because it's used on other place, I am not sure why the codes inside the mergeMap is not running. Can someone help me about it? Thanks a lot!
It won't work unless you subscribe to the results of bookService.addNewBook(book). Just returning it from the then callback won't subscribe. You need to at least add toPromise.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book).toPromise();
}).then((result) => {
console.log(result);
})
In order for the mergeMap() to be be triggered, the token$ subject inside token.service.ts needs to emit a value (via .next()) after addNewBook() is subscribed to by a consumer.
One of the things to keep in mind with Subjects is that 'late subscribers' won't receive a value off of them until the next time .next([value]) is called on that Subject. If each subscriber, no matter how late, needs to immediately receive the last value generated by that source (Subject) then you can use BehaviorSubject instead.
From your short code example it is hard to see where the Observable generated by addNewBook() is being subscribed to though. Remember, a Observable won't execute until it has a subscriber.
How can I set a delay in retryWhen?
import 'rxjs/add/operator/retry';
import 'rxjs/add/operator/retrywhen';
...
constructor(http: Http) {
var headers = new Headers();
headers.append('Content-Type', 'text/plain');
http.post('https://mywebsite.azurewebsites.net/account/getip', "", { headers: headers })
.retryWhen(errors => {
return errors.delay(1000); // errors is not a function
})
(event) => {
// handle events
this.ip = event.json();
},
(error) => {
console.error(error);
toastr.error('No connection to server. Please reload the page!')
}
);
}
I am getting the error: errors is not a function.
import {Http, Headers, Response} from '#angular/http';
http.get('http://jsonplaceholder.typicode.com/posts/1/commentsss')
.retryWhen(e => e.scan<number>((errorCount, err) => {
if (errorCount >= 10) {
throw err;
}
return errorCount + 1;
}, 0).delay(1000))
.subscribe(r => console.log(`Sub: ${r}`))
This retries for 10 times with 1 second delay.
plnkr
A little late, but this is how to do with a new version of rxjs ( >6)
I guess you are trying to automatically retry network incase of a failure.
This could be achieved via different ways, but here is a very small implementation
RetryWhen() - THis operator catches if any errors are thrown by an observable and creates errorObservable from that.
Now using the errorObservable we could retry for a specified number of attempts for a specific set of failure
For example, retries incase of a 404 failure, is really unnecessary, but incase of 500X exceptions it is really mandatory.
DelayWhen - we could use this operator to specify a timer observable that delays the next retry attempt until the time elapses. This also gives the additional advantage of lineraly increasing the delay between each retry attempts
iif - Use of this conditional operator really enables us to filter and execute the necessary observable based on a given condition. You could examples in stackoverflow as well. But I am going give a simple if else illustration
contactMap - This is higher-order observable operator, meaning it produces /maps an input/observable to an output observable. The reason for using this is, we need to redo the retry operation incase of the same failure for specified number of times and the best way to restart the operation is via an error Observable.
Note that, we could use other higher-order observable operators like mergeMap/switchMap- each have their own advantages and disadvantages, but I prefer using this. Again contactMap is different from concat operator so care should be taken here
I find the best place to implement such a retry in Angular, is inside the httpinterceptors, but this could also be done inside the service
Here is a sample implementation:
// Class to intercept all network calls and retry for X number of times
export class ErrorInterceptor implements HttpInterceptor {
// private authService: UserAuthenticationService;
private ngxLogger: NGXLogger;
private errorHandlerService: ErrorHandlerService;
constructor(injector: Injector) {
this.ngxLogger = injector.get(NGXLogger);
this.errorHandlerService = injector.get(ErrorHandlerService);
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const interceptObs$ = next.handle(req);
// you could filter for the URLS that this should be applied like this
if (req.url.match(API_FETCH_TOKEN)) {
let retryAttempts = 0;
const httpInterceptor$ = interceptObs$.pipe(
retryWhen(errorObs => {
return errorObs.pipe(
tap(errval => console.log(`Retrying because of err:${errval}`)),
concatMap(errObsValue => {
if (
errObsValue instanceof HttpErrorResponse &&
errObsValue.status != 404
) {
console.log('inside concatMap', errObsValue);
if (retryAttempts > APP_NET_RETRIES) {
return throwError(this.getErrorModel(errObsValue, req));
} else {
retryAttempts++;
// this linearly increases the delay of attempts
delayWhen(() =>
timer(retryAttempts * APP_NET_RETRIES_DELAY * 1000)
);
return httpInterceptor$;
}
}
})
);
})
);
return httpInterceptor$;
} else {
return interceptObs$;
}
// above is the notifier observable, with errors captured as input observable
// so we could operate on this observable for retires
// also make sure to return the error observable - i.e the notifier observable
// reason being all errors should again be returned so as to capture them and
// only when they are returned they can be retried
// Also make sure after take() - no.of retries we just throw a last occuring error obs
}
// I like to create a custom error model and handle that in Custom ErrorHandler
// Below is the sample implementation I use. but again this is your preference
// you can just rethrow the error alone
async getErrorModel(errValue, req): Promise<ErrorModel> {
const errModel = new ErrorModel();
errModel.Error = errValue;
errModel.ErrorMessage = 'Error in retrieving the token:' + errValue.message;
errModel.ErrorStatus = 421;
errModel.ErrorUrl = req.url;
errModel.Timestamp = momentTimeZone
.tz(DEFAULT_TIME_ZONE)
.format(DEFAULT_TIME_ZONE);
await this.errorHandlerService.handleError(errModel);
return Promise.resolve(errModel);
}
}
Hopefully, it helps..
EDIT: There is really a nice article about backoff-rxjs that really shortens eveything that we did above. Please refer this link
I found this page, and I made some improvements:
retryWhen(errors => errors.pipe(
takeWhile((err, index) => {
if (index === 3) throw new Error(err.toString());
return true
}),
delay(1000),
))