Having used Tanstack Query in the past I was excited to see an official adapter for Angular.
Given a basic application with the following files
// app-routing.module.ts
const routes: Routes = [
{
path: '',
component: HeroListComponent,
},
{
path: ':heroId',
component: HeroDetailsComponent,
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
bindToComponentInputs: true,
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
// hero.service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Hero } from './hero';
import { tap } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class HeroService {
http = inject(HttpClient);
heroes$ = this.http
.get<Hero[]>('/api/heroes')
.pipe(
tap(() => console.log(`GET /api/heroes ${new Date().toISOString()}`))
);
getHero(id: number) {
return this.http
.get<Hero>(`/api/heroes/${id}`)
.pipe(
tap(() =>
console.log(`GET /api/heroes/${id} ${new Date().toISOString()}`)
)
);
}
}
//hero-list.component.ts
import { Component, inject } from '@angular/core';
import { HeroService } from './hero.service';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';
@Component({
template: `<h2>Hero List</h2>
@if (query.isPending()) { Loading... } @if (query.error()) { An error has
occurred: {{ query.error()?.message }}
} @if (query.data(); as data) {
<ul>
@for (hero of data; track $index) {
<div>
<a [routerLink]="[hero.id]">{{ hero.name }}</a>
</div>
}
</ul>
} `,
})
export class HeroListComponent {
heroService = inject(HeroService);
query = injectQuery(() => ({
queryKey: ['heroes'],
queryFn: () => lastValueFrom(this.heroService.heroes$),
}));
}
// hero-details.component.ts
import { Component, inject, input, numberAttribute } from '@angular/core';
import { HeroService } from './hero.service';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { lastValueFrom } from 'rxjs';
@Component({
template: `<h2>Hero Detail</h2>
@if (query.isPending()) { Loading... } @if (query.error()) { An error has
occurred: {{ query.error()?.message }}
} @if (query.data(); as data) {
<div>
{{ data.name }}
</div>
} `,
})
export class HeroDetailsComponent {
heroService = inject(HeroService);
heroId = input.required({ transform: numberAttribute });
query = injectQuery(() => ({
queryKey: ['heroes', this.heroId()],
queryFn: () => lastValueFrom(this.heroService.getHero(this.heroId())),
}));
}
I wanted to use the techniques described in https://dev.to/this-is-angular/this-is-your-signal-to-try-tanstack-query-angular-35m9 to see how to reuse queries, particularly ones which depend on router params.
Creating the custom injection function and extracting out the heroQuery
import {
assertInInjectionContext,
inject,
Injector,
runInInjectionContext,
} from '@angular/core';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { HeroService } from './hero.service';
import { lastValueFrom } from 'rxjs';
export const createQuery = <T, U>(
query: (params: T) => U,
params: T,
{ injector }: { injector?: Injector } = {}
) => {
injector = assertInjector(createQuery, injector);
return runInInjectionContext(injector, () => {
return query(params);
});
};
export function assertInjector(fn: Function, injector?: Injector): Injector {
// we only call assertInInjectionContext if there is no custom injector
!injector && assertInInjectionContext(fn);
// we return the custom injector OR try get the default Injector
return injector ?? inject(Injector);
}
export function heroQuery({ heroId }: { heroId: number }) {
const solutionApi = inject(HeroService);
return injectQuery(() => ({
queryKey: ['heroes', heroId],
queryFn: () => lastValueFrom(solutionApi.getHero(heroId)),
}));
}
I can then use this in the hero-details
component with the following changes:
@Component({
template: `<h2>Hero Detail</h2>
@if(query; as query) { @if (query.isPending()) { Loading... } @if
(query.error()) { An error has occurred: {{ query.error()?.message }}
} @if (query.data(); as data) {
<div>
{{ data.name }}
</div>
} } `,
})
export class HeroDetailsComponent implements OnInit {
heroId = input.required({ transform: numberAttribute });
injector = inject(Injector);
query: ReturnType<typeof heroQuery> | null = null;
ngOnInit() {
this.query = createQuery(
heroQuery,
{
heroId: this.heroId(),
},
{ injector: this.injector }
);
}
}
However this is not as nice as the original code, we have to split the query between the component's constructor and ngOnit lifecycle hook. We also need an extra if
in the component since we need to check if the query
is null
.
We can wrap the createQuery
to tidy up the interface a bit
export const queryCreator = <T, U>(
query: (params: T) => U,
params: () => T,
{ injector }: { injector?: Injector } = {}
) => {
return {
query: null as U | null,
init: function () {
if (this.query) {
return;
}
this.query = createQuery(query, params(), { injector });
},
};
};
and update the hero-component
to
@Component({
template: `<h2>Hero Detail</h2>
@if(creator.query; as query) { @if (query.isPending()) { Loading... } @if
(query.error()) { An error has occurred: {{ query.error()?.message }}
} @if (query.data(); as data) {
<div>
{{ data.name }}
</div>
} }`,
})
export class HeroDetailsComponent {
heroId = input.required({ transform: numberAttribute });
injector = inject(Injector);
creator = queryCreator(
heroQuery,
() => ({
heroId: this.heroId(),
}),
{ injector: this.injector }
);
ngOnInit() {
this.creator.init();
}
}
However its still not great and is starting to feel like overkill at this point. Please leave a comment below if you can point me in the right direction.
Link to source code https://github.com/umar-hai/reuse-queries-demo
Top comments (0)