DEV Community

Umar Mohammed
Umar Mohammed

Posted on

How to reuse queries in Angular Query

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())),
  }));
}
Enter fullscreen mode Exit fullscreen mode

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)),
  }));
}
Enter fullscreen mode Exit fullscreen mode

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 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 });
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

Image of PulumiUP 2025

Let's talk about the current state of cloud and IaC, platform engineering, and security.

Dive into the stories and experiences of innovators and experts, from Startup Founders to Industry Leaders at PulumiUP 2025.

Register Now

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay