I am fairly new to TypeScript and RxJS, so my understanding of observables and subscriptions are limited.
I am required to call a method from the service class that returns an object array (for now, it is being logged to the console), and I am to make this call in the component class after a button is clicked.
The problem that I am running into is that the array is returning as empty in the console when I try to assign it to a variable. However, when I log the result from the http.get method directly, I see my object array.
I was wondering what I'm overlooking here to get this sort of error, and what can I do to fix it so as to avoid running into this again?
cars.model.ts
export interface ICar {
Make: string;
Model: string;
Year: string;
Specifications: boolean[];
}
cars.new.mock.json
[
{
"Make": "Honda",
"Model": "CRV",
"Year": "2021",
"Specifications": [
true,
true,
false,
true,
false
]
},
{
"Make": "Toyota",
"Model": "Camry",
"Year": "2021",
"Specifications": [
true,
true,
false,
true,
false
]
}
]
app.component.html
<h1>{{title}}</h1>
<button (click)="shownewCars()">New Cars</button>
app.component.ts
import { Component, OnInit } from '@angular/core';
import { CarsService } from './services/cars.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
title = 'AutoZone';
constructor(private carsService: CarsService){
}
ngOnInit(): void {
}
shownewCars(){
this.carsService.logNewCarsMessage();
console.log(this.carsService.returnNewCars());
}
}
cars.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from "@angular/core";
import { ICar } from '../models/cars.model';
@Injectable({
providedIn: 'root'
})
export class CarsService{
carsURL = '/assets/cars.mock.json';
newCarsURL = '/assets/cars.new.mock.json'
private newCars: ICar[] = [];
constructor(private http : HttpClient){}
returnNewCars(): ICar[]{
this.http.get<ICar[]>(this.newCarsURL).subscribe(result => {this.newCars = result});
return this.newCars;
}
logNewCarsMessage(){
this.http.get(this.newCarsURL).subscribe((responseData) => console.log("Console log New Cars: ", responseData));
}
}
Console
[]
Console log New Cars: (2)[{...}, {...}]
Your problem has to do with async code.
The http.get(...)
function returns an Observable<ICar[]>
, which will send the actual HTTP request when subscribed to.
The problem here is that the code doesn't wait till the HTTP response comes back from the server. .subscribe(result => {this.newCars = result})
registers the callback function that should be executed once the HTTP call returns, but immediately continues without actually waiting.
The next line immediately returns the (at that moment) still empty this.newCars
variable.
The reason the logNewCarsMessage
function does work, is because that one only logs once the HTTP response arrives. As you can see in your console output, the Console log New Cars: (2)[{...}, {...}]
line is printed after []
, while you call the logNewCarsMessage
function before returnNewCars
.
There are two ways you can solve this: Using Promises, or preferably using Observables.
Instead of returning the cars array directly, return an Observable that will eventually emit the cars array.
cars.service.ts
private newCars$: Observable<ICar[]>;
returnNewCars(): Observable<ICar[]> {
this.newCars$ = this.http.get<ICar[]>(this.newCarsURL);
return this.newCars$;
}
The $-sign in newCars$
is not required, but it is a nice convention to signify that the variable is actually an Observable.
app.component.ts
shownewCars(): void {
this.carsService.returnNewCars().subscribe(cars => console.log(cars));
}
You can use the .toPromise()
function to turn an Observable into a Promise. This way you can use normal async/await syntax to use your asynchronous code in a synchronous looking way.
Again, I don't really recommend this way, because while it might look easier at first glance, it is a lot less powerful and not the Angular or RxJS way.
cars.service.ts
private newCars: ICar[] = [];
async returnNewCars(): Promise<ICar[]> {
this.newCars = await this.http.get<ICar[]>(this.newCarsURL).toPromise();
return this.newCars;
}
app.component.ts
async shownewCars(): Promise<void> {
const cars = await this.carsService.returnNewCars();
console.log(cars);
}
UPDATE for RxJS 7
RxJS 7 deprecated the .toPromise()
function. As an alternative you can use lastValueFrom(myObservable$)
. The HTTP call in cars.service.ts would look like this:
this.newCars = await lastValueFrom(this.http.get<ICar[]>(this.newCarsURL));