I have a model that looks like:
export class Info {
Id: number;
Participant: object;
}
I have an endpoint which fetches a list of Info
objects, but it does not fetch the Participant
properties. I need to fetch these objects from a different service, but I'm having trouble understanding how to work with multiple Observables and one that depends on the previous value returned.
I'm fetching an array of Info
and then going through the array to fetch the Participant
object from the other service. I'm assigning the view portion of my component outside of the observable so I'm encountering issues displaying the value until the value is actually there.
Partial.html
<tr *ngFor="let connection of connections">
<td> {{connection.Id}} </td>
<td> {{connection.Participant.Name}} </td>
</tr>
Component.ts
ngOnInit() {
this.connectionInfoService.GetInfo().subscribe((connections: Info[]) => {
connections.forEach(x => {
this.participantService.getParticipantByID(x.Id).subscribe((participant) => {
x.Participant = participant;
});
});
this.connections = connections;
});
}
I get Cannot read properties of null (reading 'Name')
in the console because Name
is not a property of null
obviously until there is actually a value for Participant
. How can I handle this better? I would rather handle it within my ngOnInit
service fetching code, but I don't know how to handle an Observable that depends on another Observable like that.
CodePudding user response:
You should never have a subscribe
inside of another subscribe
. Use RxJs operators when you need to transform the data that's being emitted from an Observable.
The following code is untested, but hopefully it's close enough to get you on the right track:
import { combineLatest } from 'rxjs';
import { map, switchMap} from 'rxjs/operators';
// ...
ngOnInit() {
this.connectionInfoService.GetInfo().pipe(
switchMap(connections => {
// For each connection...
const connections$ = connections.map(connection => {
// Create an Observable to fetch the participant.
const participant$ = this.participantService.getParticipantByID(connection.Id);
return participant$.pipe(
// When the participant is recieved, add the participant
// to the connection and return the connection.
map(participant => {
connection.Participant = participant;
return connection;
})
);
});
// Use the RxJs combineLatest function to return an Observable that
// emits when all the connections$ Obervables have emitted.
return combineLatest(connections$);
})
).subscribe(connections => this.connections = connections);
}
Aside: I used the the convention of adding a trailing $
to variables that reference an Observables. You can read more about that convention here.
The key RxJs operator in this sample is the switchMap
operator. With this operator, every time the outer Observable emits, switchMap
will effectively subscribe to the inner Observable. In this case, the outer Observable is this.connectionInfoService.GetInfo()
and the inner Observable is combineLatest(connections$)
.
Bonus
Instead of subscribing to the Observable in the component class, consider using Angular's async pipe:
ngOnInit() {
this.connections$ = this.connectionInfoService.GetInfo().pipe(
// same as shown above.
);
}
<tr *ngFor="let connection of connections$ | async">
<td> {{connection.Id}} </td>
<td> {{connection.Participant.Name}} </td>
</tr>