I am trying to display data using rxResource. However, data returned is always undefined. However, inspecting the network tabs shows the data is indeed being fetched and I'm just doing something wrong on my component. What is the issue with this code? Because I can't seem to find what the problem is
API service
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { httpResource } from "@angular/common/http";
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly httpClient = inject(HttpClient);
public GET<T>(path: string, options?: any) {
return this.httpClient.get<T>(path, options);
}
Component file:
import { Component, inject, signal } from '@angular/core';
import { ApiService } from '@services/api.service';
import { rxResource, toSignal } from '@angular/core/rxjs-interop';
import { distinctUntilChanged, map } from 'rxjs';
@Component({
selector: 'app-blogs',
templateUrl: './blogs.component.html',
styleUrl: './blogs.component.scss'
})
export class BlogsComponent {
apiService = inject(ApiService)
query = signal('')
rxBlog = rxResource({
request: () => this.query(),
loader: () =>
this.apiService.GET(`https://api.nationalize.io/?name=${this.query()}`)
.pipe(
distinctUntilChanged(),
map((blogs) => blogs),
)
})
search(event: Event) {
const { value } = event.target as HTMLInputElement;
this.query.set(value);
}
}
// Template
<p>blogs works!</p>
<input (input)="search($event)" placeholder="Search user..."/>
<br />
<ul>
@let error = rxBlog.error();
@if (error) {
<p>{{ error }}</p>
}
@if (rxBlog.isLoading()) {
<p>Loading Blogs...</p>
}
@for (blog of (rxBlog.value()).country) {
{{blog.country_id}}
}
</ul>
Json data looks like below:
{
"count": 22997,
"name": "steve",
"country": [
{
"country_id": "KE",
"probability": 0.0728971981962264
},
]
}
When you want to debounce
inputs or additional reactive manipulations using rxjs
, then rxResource
is the best option.
When you want to just make an API call and have reactivity present then httpResource
is the best option.
The httpResource
under the hood calls HttpClient
so there is no need for the service.
The advantage of httpResource
is the large reduction of boiler plate code, use to make simple API calls.
You have a very basic fetch example, so rxResource
is not the best fit, instead go for httpResource
, where we only need to supply a url
inside a callback and it will reactively fetch the data when the source signals inside the callback change.
rxBlog = httpResource<ApiData>(
() => `https://api.nationalize.io/?name=${this.query()}`
);
import {
Component,
inject,
Injectable,
ResourceStatus,
signal,
} from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import {
HttpClient,
httpResource,
provideHttpClient,
} from '@angular/common/http';
import { Observable } from 'rxjs';
export interface ApiDataCountry {
country_id: string;
probability: number;
}
export interface ApiData {
count: number;
name: string;
country: Array<ApiDataCountry>;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly httpClient = inject(HttpClient);
public GET<T>(path: string, options: any = {}) {
return this.httpClient.get<T>(path, options) as Observable<T>;
}
}
@Component({
selector: 'app-root',
template: `
<p>blogs works!</p>
<input (input)="search($event)" placeholder="Search user..."/>
<br />
<ul>
@let error = rxBlog.error();
@if (error) {
<p>{{ error }}</p>
} @else if (rxBlog.isLoading()) {
<p>Loading Blogs...</p>
} @else {
@let countryList = (rxBlog.value())?.country || [];
@for (blog of countryList; track blog) {
<li>{{blog.country_id}}</li>
}
}
</ul>
`,
})
export class App {
apiService = inject(ApiService);
query = signal('');
rs = ResourceStatus;
rxBlog = httpResource<ApiData>(
() => `https://api.nationalize.io/?name=${this.query()}`
);
search(event: Event) {
const { value } = event.target as HTMLInputElement;
this.query.set(value);
}
}
bootstrapApplication(App, {
providers: [provideHttpClient()],
});
The rxResource
will not trigger the API until the source signals change, so there is no need for distinctUntilChanged
. It also fetches the values once during initialization.
rxBlog = rxResource({
request: () => this.query(),
loader: () => this.apiService.GET(`https://api.nationalize.io/?name=${this.query()}`)
})
Below is a working example to demonstrate the rxResource in action:
import {
Component,
inject,
Injectable,
ResourceStatus,
signal,
} from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface ApiDataCountry {
country_id: string;
probability: number;
}
export interface ApiData {
count: number;
name: string;
country: Array<ApiDataCountry>;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly httpClient = inject(HttpClient);
public GET<T>(path: string, options: any = {}) {
return this.httpClient.get<T>(path, options) as Observable<T>;
}
}
@Component({
selector: 'app-root',
template: `
<p>blogs works!</p>
<input (input)="search($event)" placeholder="Search user..."/>
<br />
<ul>
@let error = rxBlog.error();
@if (error) {
<p>{{ error }}</p>
} @else if (rxBlog.isLoading()) {
<p>Loading Blogs...</p>
} @else {
@let countryList = (rxBlog.value())?.country || [];
@for (blog of countryList; track blog) {
<li>{{blog.country_id}}</li>
}
}
</ul>
`,
})
export class App {
apiService = inject(ApiService);
query = signal('');
rs = ResourceStatus;
rxBlog = rxResource({
request: () => this.query(),
loader: () =>
this.apiService.GET<ApiData>(
`https://api.nationalize.io/?name=${this.query()}`
),
});
search(event: Event) {
const { value } = event.target as HTMLInputElement;
this.query.set(value);
}
}
bootstrapApplication(App, {
providers: [provideHttpClient()],
});
It is always good to define an interface for your data fetched.
export interface ApiDataCountry {
country_id: string;
probability: number;
}
export interface ApiData {
count: number;
name: string;
country: Array<ApiDataCountry>;
}
There is a possibility that the country
property fetched can be undefined
, so I use let
and or operator ||
to provide a default value if it is undefined
.
@let countryList = (rxBlog.value())?.country || [];
@for (blog of countryList; track blog) {
<li>{{blog.country_id}}</li>
}