I have service:
export class ConfigService {
private _config: BehaviorSubject<object> = new BehaviorSubject(null);
public config: Observable<object> = this._config.asObservable();
constructor(private api: APIService) {
this.loadConfigs();
}
loadConfigs() {
this.api.get('/configs').subscribe( res => this._config.next(res) );
}
}
Trying to call this from component:
...
Observable.forkJoin([someService.config])
.subscribe( res => console.log(res) ) //not working
someService.config.subscribe( res => console.log(res) ) // working
...
How can i use Observable.forkJoin with Observable variable config?
I need to store configs in service and wait unlit them and others request not finished to stop loader.
Since you're using BehaviorSubject you should know that you can call next() and complete() manually.
The forkJoin() operator emits only when all of its source Observables have emitted at least one values and they all completed. Since you're using a Subject and the asObservable method the source Observable never completes and thus the forkJoin operator never emits anything.
Btw, it doesn't make much sense to use forkJoin with just one source Observable. Also maybe have a look at zip() or combineLatest() operators that are similar and maybe it's what you need.
Two very similar question:
Observable forkJoin not firing
ForkJoin 2 BehaviorSubjects
Related
Given this method:
public logIn(data:any): Observable<any> {
this.http.get('https://api.myapp.com/csrf-cookie').subscribe(() => {
return this.http.post('https://api.myapp.com/login', data);
});
}
I would like it to return that nested observable, so that my calling code can use it like so:
this.apiService.logIn(credentials).subscribe(() => {
// redirect user to their dashboard
});
without needing to know about the first /csrf-cookie request. Obviously the above doesn't work - but I'm struggling to understand how to make the inner HTTP request wait for the outer one to finish AND be returned by the method.
you should use switchMap see the documentation on switch map
public logIn(data:any): Observable<any> {
return this.http.get('https://api.myapp.com/csrf-cookie').pipe(
switchMap(x => this.http.post('https://api.myapp.com/login', data))
);
}
with rxjs nested subscribes are generally not a good idea. There are many great operators within the library that will get you around it. In this case above where one call depends on another switchMap(...) is the best fit.
Also the code has been modified to return the observable not the subscription
I wanted to use an HTTP interceptor so that every HTTP request has a delay of 500ms between the next one. I'm currently making these requests from an injectable service that is registered on the app.module and injected in my component. In the that same module I have registered my interceptor.
// delay-interceptor.ts
#Injectable()
export class DelayInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return timer(500).pipe(
delay(500),
switchMap(() => next.handle(request))
)
}
}
// app.module.ts
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: DelayInterceptor,
multi: true
},
ManageHousesService
]
// manage-houses.component.ts
createHouses() {
this.houses.foreach((house: House) => {
this.createHousesService.createHouse(house.name).subscribe(createdHouse => {
house.rooms.foreach((room: Room) => {
this.createHousesService.createRoom(house.id, room.name).subscribe();
});
});
});
}
// manage-houses.service.ts
createHouse(houseName: string): Observable<House> {
return this.httpClient.post(`${this.apiUrl}/houses`, { houseName: houseName });
}
createRoom(houseId: string, roomName: string): Observable<Room> {
return this.httpClient.post(`${this.apiUrl}/houses/${houseId}/rooms`, { roomName: roomName });
}
In my component I have to make requests in a nested way. I have a list of houses and for each house I want to create a list of rooms. So for each house I make a POST request and on the subscription I use the ID of the newly created house to create the rooms. For each room I make a POST request with the room information and the house ID. Now this is where the issue appears. Between each house request the delay is working, but between all the rooms of a house it is not, and I can't figure out why that's happening.
I suppose it might have something to do with calling the same method inside each foreach which will probably reuse the same observable or something similar, and thus not trigger the HTTP interceptor, but I'm not sure. On the interceptor I tried to use both the timer and the delay approach, but I got the same result with both approaches.
how do you suppose that each request will take maximum 500ms?
it may take longer than that
did you try to use async/await?
you can use await to handle this asynchronous code, also it's better to avoid using forEach in asynchronous code, as forEach is not a promise-aware, that's how it has been designed
so it's better to the use the normal for loop or the ES6 for of loop
also we need to git rid of the subscribe and unsubscribe as now by using the async/await, we need to deal with promises instead of observables
for that, RxJS provides the toPromise() operator which converts an Observable to a promise so you can work with the HttpClient methods using promises instead of Observables
toPromise() returns a Promise that is resolved with the first value emitted by that Observable (it internally calls subscribe for you and wraps it with a Promise object).
you can then update the createHouses function to be something like that
async createHouses() {
for (const house of this.houses) {
// wait for the house to be added to db
await this.createHousesService.createHouse(house.name).toPromise();
// then loop over the rooms
for (const room of house.rooms) {
// wait for the room to be added to the db
await this.createHousesService.createRoom(house.id, room.name).toPromise()
}
}
}
hope it works as you needed
It is not correctly to change the behavior of the interceptor, because it will affect all requests. You can do this directly from the component, or create a service for this prototype.
concat(
...this.houses.map((house: House) =>
this.createHousesService.createHouse(house.name).pipe(
delay(500),
concatMap((createdHouse) =>
concat(
...house.rooms.map((room: Room) =>
this.createHousesService
.createRoom(house.id, room.name)
.pipe(delay(500))
)
)
)
)
)
).subscribe();
I've just finished my Angular lessons and I already find out some differences between what I learn and the Angular official documentation.
Let's imagine I want to recover an user with ID of an API.
Here is how I would do it according to my lessons :
export class UserService {
constructor(
private httpClient: HttpClient
) {
}
public user: User; // local variable using User model
public userSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
async getSingleUserFromServer() {
await this.httpClient.get<any>('https://xvalor.repliqa.fr/api/v1/user/' + this.userId).subscribe(
(response) => {
this.user = response;
this.userPortfolios = this.user.portfolioAssoc;
this.emitSubjects();
});
}
emitSubjects() {
this.userSubject.next(this.user);
}
}
and here is how angular doc procceed
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
I understand than both methods are quiet doing the same thing, I just want to be sure which one I should use, especially in big project developpement.
I would stick to the second approach as it is more generic and it uses Observable. Observale allow to emit any count of events and callback will be called for each event. Promise generates a single event after completion.
In addition, service class should not have async and await parts. The goal of service is to return data and UI component can consume data using async and await parts. async and await are syntactic sugar to avoid writing .subscribe part as it is really verbose. So write async and await in your UI components.
If you want to use Promise, then your service should not have subscribe part:
getSingleUserFromServer() {
return this.httpClient.get<any>('https://xvalor.repliqa.fr/api/v1/user/' + this.userId);
}
However, it is better to return Observables from your service.
Your first approach is flawed in that the consumer must perform two separate operations: call getSingleUserFromServer() to make the call, and subscribe to UserService.user to consume the results. And in case of errors, he won't receive any feedback.
Stick to the official guidelines for now. BTW, if your goal was to additionally store the user as a variable available to everyone, then with the Observable pattern it's as simple as adding another tap to the pipe:
httpClient.get(url)
.pipe(
someOperator(),
tap(user => this.user = user),
anotherOperator(...someArgs),
)
Observables and Subjects are two diffrent objects from rxjs and bring diffrent properties with them. The answers to this question show some of the key differences: What is the difference between a Observable and a Subject in rxjs?
I have a service called MappingService in Angular 7. This service fetches a list of names from an API:
{
'guid1': 'Item 1',
'guid2': 'Item 2',
'guide': 'Item 3',
}
These values should later be mapped by their ids. Of course I could subscribe to items$ in a componentn and then do something like:
{{ mapping[id] }}
But I thought it would be nice, If I don't have to subscribe to that service or its list so I don't have to pass it around all the time, because the mapping is fixed once loaded and is needed by multiple components.
So I thought I add a method map to the service that does this:
gets an id
returns the corresponding string
Like:
{{ mappingSrervice.map(id) }}
But this is where I'm stuck:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { mergeMap, tap, catchError } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class MappingService {
items$ = new BehaviorSubject<any>(null);
constructor(private http: HttpClient) { }
getAll(): Observable<any> {
return this.items$.pipe(mergeMap(items => {
if (items) {
return of(items);
} else {
return this.http.get<any>(
'http://api'
).pipe(
tap((response: any) => response),
catchError(error => {
return of([]);
})
);
}
}));
}
map(id: number): string {
return this.items$[id] || null;
}
}
How do I correctly implement map()? Should I run getAll() in the constructor? The problem is still of course that items$ will not be filled before getAll() is finished.
Can I get it to work like I thought? Is string the correct return type?
I wonder whether this approach is possible? Or do I need to return promises instead? But this would generate a new level of abstraction, making it not that much simpler instead of the naive approach (just subscribing to items$.
Let's first talk about caching. You already tried using a BehaviorSubject which is a very good idea. As always with async operations, there is a certain timespan when the data is not available because it is still loading.
This is not a problem and we can deal with this in our components.
There is no really cool way to do the request before the application loads (there are ways but I don't want to recommend any of them).
Talking about this, a BehaviorSubject is not the best choice, because we always need to initialize it with a value.
What is more suitable is the ReplaySubject: It keeps the last elements in its buffer and replays them to each new subscriber.
While all this is cool the way you did it, we can do easier: with the shareReplay() operator.
This makes a cold observable hot, which means: It converts the normal HTTP Observable, which does an HTTP request for each subscriber, to a shared Observable which does its task once and shares its values with all subscribers.
The code could look something like this:
export class MappingService {
items$ = this.getAll().pipe(shareReplay(1));
getAll(): Observable<any>() {
return this.http.get('http://api'); // also add catchError here as you already did
}
}
For the very first subscriber, getAll() will be called and the HTTP request will be performed. For all subsequent subscribers, the cached value will be used.
Note that you still need to subscribe to the items$ Observable, but it is not a problem or challenge to subscribe to Observables in components.
For the mapping I recommend using the RxJS map() operator.
You should provide a service method that takes an ID and returns an Observable with the result. It could look like this:
getItem(id: number): Observable<string> {
return this.items$.pipe(
map(items => items[id])
);
}
This way – using Observables everywhere – you also avoid race conditions through async operations. Each of your Observables will return data as soon as they have arrived.
Resolver may help you.
In summary, you want to delay rendering the routed component until all necessary data have been fetched.
If you return Observable or Promise in the resolve() method of your resolver, your component is not rendered until the Observable completes or the Promise is fulfilled.
In your component you can access the data provided by the resolver with route.snapshot.data.xxx.
The drawback of this approach is that it delays the rendering of your component.
Actually, I believe that it would be better to simply subscribe to items$ in this case.
I have three observable sources in my code that emit values of the same type.
const setTitle$ = params$.do(
params => this.titleService.setTitle( `${params[1].appname} - ${this.pagename}` )
).switchMap(
() => Observable.of(true)
);
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
);
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
);
When i use them in race like this
setTitle$.race(
openDocument$,
saveDocument$
).subscribe();
works only setTitle and when i subscribe manually to another two sorces like
const openDocument$ = params$.switchMap(
params => this.openDocument(params[0].id)
).subscribe();
const saveDocument$ = params$.switchMap(
params => this.saveDocument(params[0].id)
).subscribe();
then they work too. Help me understand why it's going on and how to force to work all sources in race, merge, etc.
From the documentation, the .race() operator does this:
The observable to emit first is used.
That is why, you will only get ONE emission, because only one out of the three observables that emits first will get emitted.
What you are looking for is .forkJoin() or .combineLatest().
If you want all the observables to execute in parallel and wait for ALL of them to come back as one observables, use .forkJoin():
Observable
.forkJoin([...setTitle$, openDocument$, saveDocument$])
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
//all three observables must be completed. If any of it was not completed, the other 2 observables will wait for it
})
If you however wants to listen to every emission of all the observables regardless when they are emitted, use .combineLatest():
Observable
.combineLatest(setTitle$, openDocument$, saveDocument$)
.subscribe(([setTitle, openDocument, saveDocument]) => {
//do something with your your results.
// as long as any of the observables completed, it will be emitted here.
});
Problem was with shared params source.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).share();
I have shared it with share operator. But in this article from the first comment to my question i found this:
When using multiple async pipes on streams with default values, the .share() operator might cause problems:
The share() will publish the first value of the stream on the first subscription. The first async pipe will trigger that subscription and get that initial value. The second async pipe however will subscribe after that value has already been emitted and therefore miss that value.
The solution for this problem is the .shareReplay(1) operator, which will keep track of the previous value of the stream. That way all the async pipes will get the last value.
I replaced share() with shareReplay(1) and all sources began emitting values.
const params$ = this.route.params.map(
routeParams => {
return {
id: <string>routeParams['id']
};
}
).combineLatest(
this.config.getConfig()
).shareReplay(1);
Thanks to everyone for help!