michaelxvoelker / ng-query

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 QueryClientService provider to get access to the query client instance:

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

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

Query

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

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

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

  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: [NgIf, NgForOf, 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 UseInfiniteQuery provider in your service. Using the hook is similar to the official hook, except the query function should return an observable.

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

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

  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 UsePersistedQuery provider when you want to use the keepPreviousData feature. For example, to implement the pagination functionality:

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

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

  getProjects = inject(UsePersistedQuery)((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 { QueryClientService } from '@ngneat/query';

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

  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 { QueryClientService, 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 { QueryClientService, UseMutation } from '@ngneat/query';

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

  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,
  mapResultData,
} 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));

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

Entity Utils

The library exposes the addEntity, and removeEntity helpers:

import {
  addEntity,
  QueryClientService,
  UseQuery,
  removeEntity,
} from '@ngneat/query';
import { tap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class TodosService {
  private useQuery = inject(UseQuery);
  private queryClient = inject(QueryClientService);
  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 {
  UseIsFetching,
  UseIsMutating,
  createSyncObserverResult
} from '@ngneat/query';

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

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

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

Use Constructor DI

You can use the constructor version instead of inject:

QueryService.use(...)
PersistedQueryService.use(...)
InfiniteQueryService.use(...)
MutationService.use(...)

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 { QueryClientService } from '@ngneat/query';

bootstrapApplication(AppComponent, {
  providers: [
    environment.production
      ? []
      : {
          provide: ENVIRONMENT_INITIALIZER,
          multi: true,
          useValue() {
            const queryClient = inject(QueryClientService);
            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 ❤️

About


Languages

Language:TypeScript 97.5%Language:JavaScript 1.9%Language:HTML 0.6%Language:SCSS 0.0%