Skip to content

trungvose/query

 
 

Repository files navigation

The TanStack Query (also known as react-query) adapter for Angular applications

Get rid of granular state management, manual refetching, and async spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that improve your developer experience.

Features

✅  Backend agnostic
✅  Dedicated Devtools
✅  Auto Caching
✅  Auto Refetching
✅  Window Focus Refetching
✅  Polling/Realtime Queries
✅  Parallel Queries
✅  Dependent Queries
✅  Mutations API
✅  Automatic Garbage Collection
✅  Paginated/Cursor Queries
✅  Load-More/Infinite Scroll Queries
✅  Request Cancellation
✅  Prefetching
✅  Offline Support
✅  Data Selectors


Build Status commitizen PRs coc-badge semantic-release styled with prettier spectator Join the chat at https://gitter.im/ngneat-transloco

Table of Contents

Installation

npm i @ngneat/query
yarn add @ngneat/query

Queries

Query Client

Inject the QueryClient provider to get access to the query client instance:

@Injectable({ providedIn: 'root' })
export class TodosService {
  private queryClient = inject(QueryClient);
}

Query

Inject the QueryProvider in your service. Using the hook is similar to the official hook, except the query function should return an observable.

import { QueryProvider } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private useQuery = inject(QueryProvider);

  getTodos() {
    return this.useQuery(['todos'], () => {
      return this.http.get<Todo[]>(
        'https://jsonplaceholder.typicode.com/todos'
      );
    });
  }

  getTodo(id: number) {
    return this.useQuery(['todo', id], () => {
      return this.http.get<Todo>(
        `https://jsonplaceholder.typicode.com/todos/${id}`
      );
    });
  }
}

Use it in your component:

import { SubscribeModule } from '@ngneat/subscribe';

@Component({
  standalone: true,
  imports: [CommonModule, SpinnerComponent, SubscribeModule],
  template: `
    <ng-container *subscribe="todos$ as todos">
      <ng-query-spinner *ngIf="todos.isLoading"></ng-query-spinner>

      <p *ngIf="todos.isError">Error...</p>

      <ul *ngIf="todos.isSuccess">
        <li *ngFor="let todo of todos.data">
          {{ todo.title }}
        </li>
      </ul>
    </ng-container>
  `,
})
export class TodosPageComponent {
  todos$ = inject(TodosService).getTodos().result$;
}

Note that using the *subscribe directive is optional. Subscriptions can be made using any method you choose.

Infinite Query

Inject the InfiniteQueryProvider in your service. Using the hook is similar to the official hook, except the query function should return an observable.

import { InfiniteQueryProvider } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class ProjectsService {
  private useInfiniteQuery = inject(InfiniteQueryProvider);

  getProjects() {
    return this.useInfiniteQuery(
      ['projects'],
      ({ pageParam = 0 }) => {
        return getProjects(pageParam);
      },
      {
        getNextPageParam(projects) {
          return projects.nextId;
        },
        getPreviousPageParam(projects) {
          return projects.previousId;
        },
      }
    );
  }
}

Checkout the complete example in our playground.

Persisted Query

Use the PersistedQueryProvider when you want to use the keepPreviousData feature. For example, to implement the pagination functionality:

import { inject, Injectable } from '@angular/core';
import {
  PersistedQueryProvider,
  QueryClient,
  queryOptions,
} from '@ngneat/query';
import { firstValueFrom } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class PaginationService {
  private queryClient = inject(QueryClient);

  getProjects = inject(PersistedQueryProvider)(
    (queryKey: ['projects', number]) => {
      return queryOptions({
        queryKey,
        queryFn: ({ queryKey }) => {
          return fetchProjects(queryKey[1]);
        },
      });
    }
  );

  prefetch(page: number) {
    return this.queryClient.prefetchQuery(['projects', page], () =>
      firstValueFrom(fetchProjects(page))
    );
  }
}

Checkout the complete example in our playground.

Mutations

Mutation Result

The official mutation function can be a little verbose. Generally, you can use the following in-house simplified implementation.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClient } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private queryClient = inject(QueryClient);

  addTodo({ title }: { title: string }) {
    return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
      tap((newTodo) => {
        // Invalidate to refetch
        this.queryClient.invalidateQueries(['todos']);
        // Or update manually
        this.queryClient.setQueryData<TodosResponse>(
          ['todos'],
          addEntity('todos', newTodo)
        );
      })
    );
  }
}

And in the component:

import { QueryClient, useMutationResult } from '@ngneat/query';

@Component({
  template: `
    <input #ref />

    <button
      (click)="addTodo({ title: ref.value })"
      *subscribe="addTodoMutation.result$ as addTodoMutation"
    >
      Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
    </button>
  `,
})
export class TodosPageComponent {
  private todosService = inject(TodosService);
  addTodoMutation = useMutationResult();

  addTodo({ title }) {
    this.todosService
      .addTodo({ title })
      .pipe(this.addTodoMutation.track())
      .subscribe();
  }
}

Mutation

You can use the original mutation functionality if you prefer.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { QueryClient, MutationProvider } from '@ngneat/query';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private http = inject(HttpClient);
  private queryClient = inject(QueryClient);
  private useMutation = inject(MutationProvider);

  addTodo() {
    return this.useMutation(({ title }: { title: string }) => {
      return this.http.post<{ success: boolean }>(`todos`, { title }).pipe(
        tap((newTodo) => {
          // Invalidate to refetch
          this.queryClient.invalidateQueries(['todos']);
          // Or update manually
          this.queryClient.setQueryData<TodosResponse>(
            ['todos'],
            addEntity('todos', newTodo)
          );
        })
      );
    });
  }
}

And in the component:

@Component({
  template: `
    <input #ref />

    <button
      (click)="addTodo({ title: ref.value })"
      *subscribe="addTodoMutation.result$ as addTodoMutation"
    >
      Add todo {{ addTodoMutation.isLoading ? 'Loading' : '' }}
    </button>
  `,
})
export class TodosPageComponent {
  private todosService = inject(TodosService);
  addTodoMutation = this.todosService.addTodo();

  addTodo({ title }) {
    this.addTodoMutation$.mutate({ title }).then((res) => {
      console.log(res.success);
    });
  }
}

Query Global Options

You can provide the QUERY_CLIENT_OPTIONS provider to set the global options of the query client instance:

import { QUERY_CLIENT_OPTIONS } from '@ngneat/query';

{
  provide: QUERY_CLIENT_OPTIONS,
  useValue: {
    defaultOptions: {
      queries: {
        staleTime: 3000
      }
    }
  }
}

Note that the default staleTime of this library is Infinity.

Operators

import { filterError, filterSuccess, selectResult } from '@ngneat/query';

export class TodosPageComponent {
  todosService = inject(TodosService);

  ngOnInit() {
    this.todosService.getTodos().result$.pipe(filterError());
    this.todosService.getTodos().result$.pipe(filterSuccess());
    this.todosService
      .getTodos()
      .result$.pipe(selectResult((result) => result.data.foo));
  }
}

Entity Utils

The library exposes the addEntity, and removeEntity helpers:

import {
  addEntity,
  QueryClient,
  QueryProvider,
  removeEntity,
} from '@ngneat/query';
import { tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private useQuery = inject(QueryProvider);
  private queryClient = inject(QueryClient);
  private http = inject(HttpClient);

  createTodo(body) {
    return this.http.post('todos', body).pipe(
      tap((newTodo) => {
        this.queryClient.setQueryData<TodosResponse>(
          ['todos'],
          addEntity('todos', newTodo)
        );
      })
    );
  }
}

Utils

Implementation of isFetching and isMutating.

import {
  IsFetchingProvider,
  IsMutatingProvider,
  createSyncObserverResult,
  mapResultData
} from '@ngneat/query';

// How many queries are fetching?
const isFetching$ = inject(IsFetchingProvider)();
// How many queries matching the posts prefix are fetching?
const isFetchingPosts$ = inject(IsFetchingProvider)(['posts']);

// How many mutations are fetching?
const isMutating$ = inject(IsMutatingProvider)();
// How many mutations matching the posts prefix are fetching?
const isMutatingPosts$ = inject(IsMutatingProvider)(['posts']);

// Create sync successfull observer in case we want to work with one interface
of(createSyncObserverResult(data, options?))

// Map the result `data`
getTodos().pipe(
  mapResultData(data => {
    return {
      todos: data.todos.filter(predicate)
    }
}))

Devtools

Install the @ngneat/query-devtools package. Lazy load and use it only in development environment:

import { ENVIRONMENT_INITIALIZER } from '@angular/core';
import { environment } from './environments/environment';

import { QueryClient } from '@ngneat/query';

bootstrapApplication(AppComponent, {
  providers: [
    environment.production
      ? []
      : {
          provide: ENVIRONMENT_INITIALIZER,
          multi: true,
          useValue() {
            const queryClient = inject(QueryClient);
            import('@ngneat/query-devtools').then((m) => {
              m.ngQueryDevtools({ queryClient });
            });
          },
        },
  ],
});

Created By

Netanel Basal
Netanel Basal

Contributors ✨

Thank goes to all these wonderful people who contributed ❤️

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 96.9%
  • JavaScript 2.4%
  • Other 0.7%